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,364 @@
1
+ """debug-wizard brain — the diagnosis oracle. Never mutates; only proposes."""
2
+
3
+ import ast
4
+ import builtins
5
+ import datetime as dt
6
+ import json
7
+ import re
8
+ from collections import Counter
9
+ from pathlib import Path
10
+
11
+ HOME = Path.home()
12
+ BANK = HOME / ".debug-wizard"
13
+ IGNORE_LIST = BANK / "ignored.json"
14
+
15
+ DEBUG_LAW = """DEBUG LAW (Debug Wizard, the diagnosis oracle)
16
+ 1. Diagnose freely: autopsy tracebacks, classify errors, count hotspots, report facts.
17
+ 2. Never mutate: no writes, no patches, no fixes applied — only proposals staged for the Write Wizard.
18
+ 3. Consent wall: any fix suggestion goes through write_wizard propose; Governor consents.
19
+ 4. Plain verdicts: [OK]/[FAIL]/out of scope, exit 0/1/2, no tracebacks.
20
+ 5. Truth first: run python_wizard check or python -m py_compile before analysis.
21
+ 6. Ignore list amends only by Governor decree; protected paths stay ignored.
22
+ """
23
+
24
+ def _ensure_bank():
25
+ BANK.mkdir(parents=True, exist_ok=True)
26
+ if not IGNORE_LIST.exists():
27
+ IGNORE_LIST.write_text("[]")
28
+
29
+ def load_ignored() -> list[str]:
30
+ _ensure_bank()
31
+ try:
32
+ return json.loads(IGNORE_LIST.read_text())
33
+ except (json.JSONDecodeError, OSError):
34
+ return []
35
+
36
+ def add_ignore(path_str: str) -> dict:
37
+ target = Path(path_str).expanduser().resolve()
38
+ ign = load_ignored()
39
+ if str(target) in ign:
40
+ return {"ok": False, "error": f"already ignored: {target}"}
41
+ ign.append(str(target))
42
+ IGNORE_LIST.write_text(json.dumps(ign, indent=2))
43
+ return {"ok": True, "ignored": str(target)}
44
+
45
+ def remove_ignore(path_str: str) -> dict:
46
+ target = Path(path_str).expanduser().resolve()
47
+ ign = load_ignored()
48
+ if str(target) not in ign:
49
+ return {"ok": False, "error": f"not ignored: {target}"}
50
+ ign.remove(str(target))
51
+ IGNORE_LIST.write_text(json.dumps(ign, indent=2))
52
+ return {"ok": True, "unignored": str(target)}
53
+
54
+ def _walk_py_files(root: Path):
55
+ root = Path(root)
56
+ if not root.exists():
57
+ return
58
+ if root.is_file() and root.suffix == ".py":
59
+ yield root
60
+ return
61
+
62
+ ignored = [Path(p) for p in load_ignored()]
63
+ for p in sorted(root.rglob("*.py")):
64
+ if any(part in {".venv", ".git", "__pycache__"} for part in p.parts):
65
+ continue
66
+ resolved = p.resolve()
67
+ if any(resolved == item or item in resolved.parents for item in ignored):
68
+ continue
69
+ yield p
70
+
71
+ def _compile_errors(file: Path) -> list[dict]:
72
+ """Return list of compilation errors without tracebacks."""
73
+ errors = []
74
+ try:
75
+ with file.open("r", encoding="utf-8", errors="replace") as f:
76
+ source = f.read()
77
+ ast.parse(source)
78
+ except SyntaxError as e:
79
+ errors.append({
80
+ "line": e.lineno or 1,
81
+ "type": "SyntaxError",
82
+ "message": e.msg or "invalid syntax",
83
+ "col": e.offset or 0,
84
+ "snippet": e.text.strip() if e.text else ""
85
+ })
86
+ except Exception as e:
87
+ errors.append({"line": 0, "type": type(e).__name__, "message": str(e)})
88
+ return errors
89
+
90
+ def _classify(errors: list[dict]) -> dict[str, int]:
91
+ counts = Counter(e["type"] for e in errors)
92
+ return dict(counts)
93
+
94
+ _MODULE_DUNDERS = frozenset(
95
+ {"__file__", "__name__", "__doc__", "__package__", "__spec__",
96
+ "__loader__", "__builtins__", "__path__", "__cached__"}
97
+ )
98
+
99
+
100
+ def _missing_imports(file: Path) -> list[str]:
101
+ """Names read but never bound anywhere in the file (likely missing imports)."""
102
+ try:
103
+ tree = ast.parse(file.read_text(encoding="utf-8", errors="replace"))
104
+ except (SyntaxError, ValueError, OSError):
105
+ return []
106
+ bound: set[str] = set(dir(builtins)) | _MODULE_DUNDERS
107
+ for node in ast.walk(tree):
108
+ if isinstance(node, ast.ImportFrom) and any(a.name == "*" for a in node.names):
109
+ return [] # star import: cannot know what it defines
110
+ if isinstance(node, ast.Import):
111
+ for alias in node.names:
112
+ bound.add(alias.asname or alias.name.split(".")[0])
113
+ elif isinstance(node, ast.ImportFrom):
114
+ for alias in node.names:
115
+ bound.add(alias.asname or alias.name)
116
+ elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
117
+ bound.add(node.name)
118
+ elif isinstance(node, ast.arg):
119
+ bound.add(node.arg)
120
+ elif isinstance(node, ast.Name) and isinstance(node.ctx, (ast.Store, ast.Del)):
121
+ bound.add(node.id)
122
+ elif isinstance(node, ast.ExceptHandler) and node.name:
123
+ bound.add(node.name)
124
+ elif isinstance(node, (ast.Global, ast.Nonlocal)):
125
+ bound.update(node.names)
126
+ elif isinstance(node, (ast.MatchAs, ast.MatchStar)) and node.name:
127
+ bound.add(node.name)
128
+ elif isinstance(node, ast.MatchMapping) and node.rest:
129
+ bound.add(node.rest)
130
+ elif type(node).__name__ in ("TypeVar", "ParamSpec", "TypeVarTuple"):
131
+ bound.add(str(getattr(node, "name", "")))
132
+ used = {
133
+ node.id
134
+ for node in ast.walk(tree)
135
+ if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load)
136
+ }
137
+ return sorted(used - bound)
138
+
139
+
140
+ def _signature_issues(file: Path) -> list[dict]:
141
+ """Detect call vs definition mismatches."""
142
+ issues = []
143
+ try:
144
+ tree = ast.parse(file.read_text(encoding="utf-8", errors="replace"))
145
+ except SyntaxError:
146
+ return [{"line": 0, "type": "SyntaxError", "message": "file unparseable"}]
147
+
148
+ defs = {node.name: {"args": len(node.args.args), "lineno": node.lineno}
149
+ for node in ast.walk(tree) if isinstance(node, ast.FunctionDef)}
150
+ calls = [(node.func.id, len(node.args), node.lineno)
151
+ for node in ast.walk(tree) if isinstance(node, ast.Call) and isinstance(node.func, ast.Name)]
152
+
153
+ for func_name, call_args, lineno in calls:
154
+ if func_name in defs and defs[func_name]["args"] != call_args:
155
+ issues.append({"line": lineno, "type": "SignatureMismatch",
156
+ "message": f"{func_name} def={defs[func_name]['args']} args, call={call_args}"})
157
+ return issues
158
+
159
+ _TB_FRAME = re.compile(
160
+ r'^\s*File "(?P<file>.+)", line (?P<line>\d+)(?:, in (?P<func>.*))?$')
161
+ _TB_EXC = re.compile(
162
+ r"^(?P<type>[A-Za-z_][\w.]*)\s*(?::\s*(?P<msg>.*))?$")
163
+
164
+
165
+ def _parse_traceback(text: str) -> dict | None:
166
+ """Parse a real Python traceback pasted/saved in a file, or None.
167
+
168
+ A traceback is its header line, at least one File frame, and a
169
+ final exception line. Anything else is not a traceback and the
170
+ caller falls back to the compile check.
171
+ """
172
+ lines = text.splitlines()
173
+ if not any(l.strip().startswith("Traceback (most recent call last)")
174
+ for l in lines):
175
+ return None
176
+ frames = []
177
+ for l in lines:
178
+ m = _TB_FRAME.match(l)
179
+ if m:
180
+ frames.append({"file": m.group("file"),
181
+ "line": int(m.group("line")),
182
+ "func": (m.group("func") or "").strip()})
183
+ exc = None
184
+ for l in reversed(lines):
185
+ s = l.strip()
186
+ if not s or s.startswith("File ") or s.startswith("Traceback"):
187
+ continue
188
+ m = _TB_EXC.match(s)
189
+ if m and (m.group("type")[0].isupper() or "." in m.group("type")):
190
+ exc = {"type": m.group("type"), "message": m.group("msg") or ""}
191
+ break # only the last content line can be the exception line
192
+ if not frames or exc is None:
193
+ return None
194
+ return {"frames": frames, "type": exc["type"], "message": exc["message"]}
195
+
196
+
197
+ def _read_text(file: Path) -> str:
198
+ try:
199
+ return Path(file).read_text(encoding="utf-8", errors="replace")
200
+ except OSError:
201
+ return ""
202
+
203
+
204
+ def _frame_home(file: str) -> str:
205
+ """'library' for stdlib / site-packages frames, else 'project'."""
206
+ import sysconfig
207
+ f = file.replace("\\", "/")
208
+ roots = [sysconfig.get_path("stdlib"), sysconfig.get_path("purelib"),
209
+ sysconfig.get_path("platlib")]
210
+ if "/site-packages/" in f:
211
+ return "library"
212
+ if any(r and f.startswith(r.replace("\\", "/")) for r in roots):
213
+ return "library"
214
+ return "project"
215
+
216
+
217
+ def stack_digest(text: str) -> dict | None:
218
+ """Digest a saved traceback: exception, frame chain, deepest frames.
219
+
220
+ Returns None when the text is not a traceback. The deepest
221
+ project frame is the last frame outside the library — where the
222
+ Governor's own code was standing when it fell. A frame repeated
223
+ three or more times is flagged as suspected recursion.
224
+ """
225
+ tb = _parse_traceback(text)
226
+ if tb is None:
227
+ return None
228
+ frames = [{**fr, "home": _frame_home(fr["file"])} for fr in tb["frames"]]
229
+ counts = Counter((f["file"], f["line"], f["func"]) for f in frames)
230
+ repeated = [(k, c) for k, c in counts.most_common() if c >= 3]
231
+ project = [f for f in frames if f["home"] == "project"]
232
+ return {"type": tb["type"], "message": tb["message"],
233
+ "frames": frames,
234
+ "deepest": frames[-1],
235
+ "deepest_project": project[-1] if project else None,
236
+ "repeated": repeated}
237
+
238
+
239
+ def trace(file: Path) -> dict:
240
+ """Autopsy one file: a saved traceback if it is one, else compilation errors."""
241
+ tb = _parse_traceback(_read_text(file))
242
+ if tb is not None:
243
+ last = tb["frames"][-1]
244
+ return {"file": str(file), "ok": False, "traceback": tb,
245
+ "errors": [{"line": last["line"], "type": tb["type"],
246
+ "message": tb["message"], "col": 0, "snippet": ""}]}
247
+ errors = _compile_errors(file)
248
+ return {"file": str(file), "errors": errors, "ok": len(errors) == 0}
249
+
250
+ def classify_file(file: Path) -> dict:
251
+ """Error type breakdown."""
252
+ tb = _parse_traceback(_read_text(file))
253
+ if tb is not None:
254
+ return {"file": str(file), "classification": {tb["type"]: 1}, "total": 1}
255
+ errors = _compile_errors(file)
256
+ return {"file": str(file), "classification": _classify(errors), "total": len(errors)}
257
+
258
+ def locate(root: Path, pattern: str) -> list[str]:
259
+ """Find files whose contents contain the pattern, or whose errors match it."""
260
+ hits = []
261
+ for f in _walk_py_files(root):
262
+ if pattern in _read_text(f):
263
+ hits.append(str(f))
264
+ continue
265
+ err = trace(f)
266
+ for e in err.get("errors", []):
267
+ if pattern.lower() in e["message"].lower() or pattern in e["type"]:
268
+ hits.append(str(f))
269
+ break
270
+ return hits
271
+
272
+ def count_dir(root: Path) -> dict:
273
+ """Errors by category."""
274
+ all_errors = []
275
+ for f in _walk_py_files(root):
276
+ all_errors.extend(_compile_errors(f))
277
+ return {"totals": _classify(all_errors), "files_checked": sum(1 for _ in _walk_py_files(root))}
278
+
279
+ def hotspot(root: Path) -> list[tuple[str, int]]:
280
+ """Files with most errors, ranked."""
281
+ scores = []
282
+ for f in _walk_py_files(root):
283
+ err_count = len(_compile_errors(f))
284
+ if err_count > 0:
285
+ scores.append((str(f), err_count))
286
+ return sorted(scores, key=lambda x: -x[1])[:10]
287
+
288
+ def timeline(root: Path) -> list[dict]:
289
+ """Error frequency by modification time."""
290
+ data = []
291
+ for f in _walk_py_files(root):
292
+ err_count = len(_compile_errors(f))
293
+ if err_count > 0:
294
+ data.append({
295
+ "file": str(f),
296
+ "errors": err_count,
297
+ "mtime": f.stat().st_mtime
298
+ })
299
+ data.sort(key=lambda x: -x["mtime"])
300
+ return data
301
+
302
+ def imports(file: Path) -> dict:
303
+ """Missing import detection."""
304
+ missing = _missing_imports(file)
305
+ return {"file": str(file), "likely_missing": missing}
306
+
307
+ def names(file: Path) -> list[str]:
308
+ """Undefined names."""
309
+ return _missing_imports(file)
310
+
311
+ def signatures(file: Path) -> list[dict]:
312
+ """Function signature issues."""
313
+ return _signature_issues(file)
314
+
315
+ def suggest_fix(file: Path) -> str:
316
+ """Human-readable fix suggestion (no AI — heuristic)."""
317
+ tb = _parse_traceback(_read_text(file))
318
+ if tb is not None:
319
+ last = tb["frames"][-1]
320
+ lines = [f"[TRACEBACK] {file}"]
321
+ for fr in tb["frames"]:
322
+ lines.append(f" File \"{fr['file']}\", line {fr['line']}, in {fr['func']}")
323
+ lines.append(f" {tb['type']}: {tb['message']}" if tb["message"]
324
+ else f" {tb['type']}")
325
+ lines.append(f"raised at {last['file']}:{last['line']} in {last['func']} "
326
+ f"(innermost of {len(tb['frames'])} frame(s)) — start there.")
327
+ return "\n".join(lines)
328
+ errs = trace(file)
329
+ if errs["ok"]:
330
+ return f"[OK] {file}: no compilation errors"
331
+ lines = [f"[ISSUE] {file}"]
332
+ for e in errs["errors"][:5]:
333
+ lines.append(f" Line {e['line']}: {e['type']} — {e['message']}")
334
+ if errs["errors"][5:]:
335
+ lines.append(f" ... and {len(errs['errors']) - 5} more errors")
336
+ return "\n".join(lines)
337
+
338
+ def audit_dir(root: Path) -> dict:
339
+ """Full error census."""
340
+ files = list(_walk_py_files(root))
341
+ healthy = 0
342
+ unhealthy = 0
343
+ total_errors = 0
344
+ for f in files:
345
+ err = trace(f)
346
+ if err["ok"]:
347
+ healthy += 1
348
+ else:
349
+ unhealthy += 1
350
+ total_errors += len(err["errors"])
351
+ return {"healthy": healthy, "unhealthy": unhealthy, "total_errors": total_errors,
352
+ "files_checked": len(files), "health_pct": round(healthy / max(len(files), 1) * 100, 1)}
353
+
354
+ def report_json(root: Path) -> str:
355
+ """JSON report for CI/CD."""
356
+ data = {"timestamp": dt.datetime.now().isoformat(timespec="seconds"), "root": str(root),
357
+ "audit": audit_dir(root),
358
+ "hotspots": [{"file": f, "errors": e} for f, e in hotspot(root)]}
359
+ return json.dumps(data, indent=2)
360
+
361
+ def stats_fleet() -> dict:
362
+ """Fleet statistics derivable anywhere: the current directory's error census."""
363
+ root = Path.cwd()
364
+ return {str(root): audit_dir(root)}
File without changes
@@ -0,0 +1,21 @@
1
+ """Entry point for wiz dispatch."""
2
+ import sys
3
+ from .lib import cli as internal_cli
4
+
5
+ # Wrap main() to accept argv (wiz dispatcher calls main(args))
6
+ def main(argv=None):
7
+ # Save original sys.argv
8
+ old_argv = sys.argv
9
+ # Replace with provided argv (or empty list)
10
+ if argv is not None:
11
+ sys.argv = [sys.argv[0]] + list(argv)
12
+ try:
13
+ return internal_cli.main()
14
+ finally:
15
+ sys.argv = old_argv
16
+
17
+ # Also expose for module-level access
18
+ __all__ = ['main']
19
+
20
+ if __name__ == "__main__":
21
+ sys.exit(main())
@@ -0,0 +1,134 @@
1
+ """diff-wizard CLI front-end."""
2
+ import sys
3
+ from pathlib import Path
4
+ from .lib.diff_wizard import DiffWizard
5
+
6
+ def main() -> int:
7
+ args = sys.argv[1:]
8
+ if not args or args[0] in ("-h", "--help", "help"):
9
+ print("Diff Wizard — standalone change-truth doctor")
10
+ print("usage: diffwiz <command> [args]")
11
+ print(" files <a> <b> unified diff of two files")
12
+ print(" dirs <a> <b> compare directory trees")
13
+ print(" context <N> <a> <b> unified diff with N context lines")
14
+ print(" summary <a> <b> diff statistics without full output")
15
+ print(" word <a> <b> word-level diff")
16
+ print(" same <a> <b> exit 0 if identical, 1 if different (silent)")
17
+ print(" reverse <a> <b> diff b -> a (swap sides)")
18
+ return 0
19
+
20
+ wiz = DiffWizard()
21
+
22
+ # --- existing commands ---
23
+ if args[0] == "files":
24
+ if len(args) < 3:
25
+ print("out of scope")
26
+ return 2
27
+ a, b = Path(args[1]), Path(args[2])
28
+ for p in (a, b):
29
+ if not p.exists():
30
+ print(f"[FAIL] not found: {p}")
31
+ return 1
32
+ text = wiz.diff_files(a, b)
33
+ if not text:
34
+ print("[OK] files identical")
35
+ return 0
36
+ print(text)
37
+ print("[FAIL] files differ")
38
+ return 1
39
+ if args[0] == "dirs":
40
+ if len(args) < 3:
41
+ print("out of scope")
42
+ return 2
43
+ da, db = Path(args[1]), Path(args[2])
44
+ for p in (da, db):
45
+ if not p.is_dir():
46
+ print(f"[FAIL] not a directory: {p}")
47
+ return 1
48
+ r = wiz.diff_dirs(da, db)
49
+ count = len(r["only_a"]) + len(r["only_b"]) + len(r["differing"])
50
+ for rel in r["only_a"]:
51
+ print(f"only in {args[1]}: {rel}")
52
+ for rel in r["only_b"]:
53
+ print(f"only in {args[2]}: {rel}")
54
+ for rel in r["differing"]:
55
+ print(f"differing: {rel}")
56
+ if count == 0:
57
+ print("[OK] trees identical")
58
+ return 0
59
+ print(f"[FAIL] {count} differences")
60
+ return 1
61
+
62
+ # --- new commands ---
63
+ if args[0] == "context":
64
+ if len(args) < 4:
65
+ print("out of scope - context requires N a b")
66
+ return 2
67
+ n = int(args[1])
68
+ a, b = Path(args[2]), Path(args[3])
69
+ for p in (a, b):
70
+ if not p.exists():
71
+ print(f"[FAIL] not found: {p}")
72
+ return 1
73
+ text = wiz.diff_files_context(a, b, n)
74
+ if not text:
75
+ print("[OK] files identical")
76
+ return 0
77
+ print(text)
78
+ print("[FAIL] files differ")
79
+ return 1
80
+ if args[0] == "summary":
81
+ if len(args) < 3:
82
+ print("out of scope")
83
+ return 2
84
+ a, b = Path(args[1]), Path(args[2])
85
+ for p in (a, b):
86
+ if not p.exists():
87
+ print(f"[FAIL] not found: {p}")
88
+ return 1
89
+ r = wiz.diff_summary(a, b)
90
+ if r["identical"]:
91
+ print("[OK] identical")
92
+ return 0
93
+ print(f"added={r['added_lines']} removed={r['removed_lines']} hunks={r['hunks']}")
94
+ return 1
95
+ if args[0] == "word":
96
+ if len(args) < 3:
97
+ print("out of scope")
98
+ return 2
99
+ a, b = Path(args[1]), Path(args[2])
100
+ for p in (a, b):
101
+ if not p.exists():
102
+ print(f"[FAIL] not found: {p}")
103
+ return 1
104
+ print(wiz.diff_word_level(a, b))
105
+ return 0
106
+ if args[0] == "same":
107
+ if len(args) < 3:
108
+ print("out of scope")
109
+ return 2
110
+ a, b = Path(args[1]), Path(args[2])
111
+ for p in (a, b):
112
+ if not p.exists():
113
+ print(f"[FAIL] not found: {p}")
114
+ return 1
115
+ return 0 if wiz.files_identical(a, b) else 1
116
+ if args[0] == "reverse":
117
+ if len(args) < 3:
118
+ print("out of scope")
119
+ return 2
120
+ a, b = Path(args[1]), Path(args[2])
121
+ for p in (a, b):
122
+ if not p.exists():
123
+ print(f"[FAIL] not found: {p}")
124
+ return 1
125
+ text = wiz.diff_files_reverse(a, b)
126
+ if not text:
127
+ print("[OK] files identical")
128
+ return 0
129
+ print(text)
130
+ print("[FAIL] files differ")
131
+ return 1
132
+
133
+ print("out of scope")
134
+ return 2
@@ -0,0 +1 @@
1
+ # wizard package init
@@ -0,0 +1,144 @@
1
+ """diff-wizard CLI front-end."""
2
+ import sys
3
+ from pathlib import Path
4
+ from .diff_wizard import DiffWizard
5
+
6
+
7
+ def main(argv=None) -> int:
8
+ args = sys.argv[1:] if argv is None else list(argv)
9
+ if not args or args[0] in ("-h", "--help", "help"):
10
+ print("Diff Wizard — standalone change-truth doctor")
11
+ print("alias: diffwiz")
12
+ print("usage: diff_wizard <command> [args]")
13
+ print(" files <a> <b> unified diff of two files")
14
+ print(" dirs <a> <b> compare directory trees")
15
+ print(" context <N> <a> <b> unified diff with N context lines")
16
+ print(" summary <a> <b> diff statistics without full output")
17
+ print(" word <a> <b> word-level diff")
18
+ print(" same <a> <b> exit 0 if identical, 1 if different (silent)")
19
+ print(" reverse <a> <b> diff b -> a (swap sides)")
20
+ return 0
21
+
22
+ wiz = DiffWizard()
23
+
24
+ # --- existing commands ---
25
+ if args[0] == "files":
26
+ if len(args) < 3:
27
+ print("out of scope")
28
+ return 2
29
+ a, b = Path(args[1]), Path(args[2])
30
+ for p in (a, b):
31
+ if not p.exists():
32
+ print(f"[FAIL] not found: {p}")
33
+ return 1
34
+ text = wiz.diff_files(a, b)
35
+ if not text:
36
+ print("[OK] files identical")
37
+ return 0
38
+ print(text)
39
+ print("[FAIL] files differ")
40
+ return 1
41
+
42
+ if args[0] == "dirs":
43
+ if len(args) < 3:
44
+ print("out of scope")
45
+ return 2
46
+ da, db = Path(args[1]), Path(args[2])
47
+ for p in (da, db):
48
+ if not p.is_dir():
49
+ print(f"[FAIL] not a directory: {p}")
50
+ return 1
51
+ r = wiz.diff_dirs(da, db)
52
+ count = len(r["only_a"]) + len(r["only_b"]) + len(r["differing"])
53
+ for rel in r["only_a"]:
54
+ print(f"only in {args[1]}: {rel}")
55
+ for rel in r["only_b"]:
56
+ print(f"only in {args[2]}: {rel}")
57
+ for rel in r["differing"]:
58
+ print(f"differing: {rel}")
59
+ if count == 0:
60
+ print("[OK] trees identical")
61
+ return 0
62
+ print(f"[FAIL] {count} differences")
63
+ return 1
64
+
65
+ # --- new commands ---
66
+ if args[0] == "context":
67
+ if len(args) < 4:
68
+ print("out of scope - context requires N a b")
69
+ return 2
70
+ n = int(args[1])
71
+ a, b = Path(args[2]), Path(args[3])
72
+ for p in (a, b):
73
+ if not p.exists():
74
+ print(f"[FAIL] not found: {p}")
75
+ return 1
76
+ text = wiz.diff_files_context(a, b, n)
77
+ if not text:
78
+ print("[OK] files identical")
79
+ return 0
80
+ print(text)
81
+ print("[FAIL] files differ")
82
+ return 1
83
+
84
+ if args[0] == "summary":
85
+ if len(args) < 3:
86
+ print("out of scope")
87
+ return 2
88
+ a, b = Path(args[1]), Path(args[2])
89
+ for p in (a, b):
90
+ if not p.exists():
91
+ print(f"[FAIL] not found: {p}")
92
+ return 1
93
+ if not p.is_file():
94
+ print(f"[FAIL] not a file: {p}")
95
+ return 1
96
+ r = wiz.diff_summary(a, b)
97
+ if r["identical"]:
98
+ print("[OK] identical")
99
+ return 0
100
+ print(f"added={r['added_lines']} removed={r['removed_lines']} hunks={r['hunks']}")
101
+ return 1
102
+
103
+ if args[0] == "word":
104
+ if len(args) < 3:
105
+ print("out of scope")
106
+ return 2
107
+ a, b = Path(args[1]), Path(args[2])
108
+ for p in (a, b):
109
+ if not p.exists():
110
+ print(f"[FAIL] not found: {p}")
111
+ return 1
112
+ print(wiz.diff_word_level(a, b))
113
+ return 0
114
+
115
+ if args[0] == "same":
116
+ if len(args) < 3:
117
+ print("out of scope")
118
+ return 2
119
+ a, b = Path(args[1]), Path(args[2])
120
+ for p in (a, b):
121
+ if not p.exists():
122
+ print(f"[FAIL] not found: {p}")
123
+ return 1
124
+ return 0 if wiz.files_identical(a, b) else 1
125
+
126
+ if args[0] == "reverse":
127
+ if len(args) < 3:
128
+ print("out of scope")
129
+ return 2
130
+ a, b = Path(args[1]), Path(args[2])
131
+ for p in (a, b):
132
+ if not p.exists():
133
+ print(f"[FAIL] not found: {p}")
134
+ return 1
135
+ text = wiz.diff_files_reverse(a, b)
136
+ if not text:
137
+ print("[OK] files identical")
138
+ return 0
139
+ print(text)
140
+ print("[FAIL] files differ")
141
+ return 1
142
+
143
+ print("out of scope")
144
+ return 2