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.
- wiz_central_toolkit/__init__.py +2 -0
- wiz_central_toolkit/bannerwiz/__main__.py +6 -0
- wiz_central_toolkit/bannerwiz/cli.py +79 -0
- wiz_central_toolkit/bannerwiz/lib/__init__.py +1 -0
- wiz_central_toolkit/bannerwiz/lib/banner_wizard.py +999 -0
- wiz_central_toolkit/bannerwiz/lib/banner_wrapper.py +199 -0
- wiz_central_toolkit/bannerwiz/lib/cli.py +79 -0
- wiz_central_toolkit/codeguardwiz/__init__.py +1 -0
- wiz_central_toolkit/codeguardwiz/__main__.py +19 -0
- wiz_central_toolkit/codeguardwiz/cli.py +971 -0
- wiz_central_toolkit/codeguardwiz/codeguard_wizard.py +269 -0
- wiz_central_toolkit/codeguardwiz/rust_engine.py +348 -0
- wiz_central_toolkit/codeguardwiz/static_engine.py +307 -0
- wiz_central_toolkit/common/__init__.py +1 -0
- wiz_central_toolkit/debugwiz/__init__.py +1 -0
- wiz_central_toolkit/debugwiz/__main__.py +360 -0
- wiz_central_toolkit/debugwiz/debug_wizard.py +364 -0
- wiz_central_toolkit/diffwiz/__init__.py +0 -0
- wiz_central_toolkit/diffwiz/__main__.py +21 -0
- wiz_central_toolkit/diffwiz/cli.py +134 -0
- wiz_central_toolkit/diffwiz/lib/__init__.py +1 -0
- wiz_central_toolkit/diffwiz/lib/cli.py +144 -0
- wiz_central_toolkit/diffwiz/lib/diff_wizard.py +89 -0
- wiz_central_toolkit/godfatherwiz/__init__.py +0 -0
- wiz_central_toolkit/godfatherwiz/__main__.py +18 -0
- wiz_central_toolkit/godfatherwiz/ai.py +136 -0
- wiz_central_toolkit/godfatherwiz/lib/__init__.py +1 -0
- wiz_central_toolkit/godfatherwiz/lib/cli.py +925 -0
- wiz_central_toolkit/godfatherwiz/lib/godfather_wizard.py +1552 -0
- wiz_central_toolkit/jsonwiz/__init__.py +0 -0
- wiz_central_toolkit/jsonwiz/__main__.py +21 -0
- wiz_central_toolkit/jsonwiz/cli.py +4 -0
- wiz_central_toolkit/jsonwiz/lib/__init__.py +1 -0
- wiz_central_toolkit/jsonwiz/lib/cli.py +138 -0
- wiz_central_toolkit/jsonwiz/lib/json_wizard.py +133 -0
- wiz_central_toolkit/pythonwiz/__init__.py +0 -0
- wiz_central_toolkit/pythonwiz/__main__.py +21 -0
- wiz_central_toolkit/pythonwiz/lib/__init__.py +1 -0
- wiz_central_toolkit/pythonwiz/lib/cli.py +247 -0
- wiz_central_toolkit/pythonwiz/lib/python_wizard.py +353 -0
- wiz_central_toolkit/readwiz/__init__.py +5 -0
- wiz_central_toolkit/readwiz/__main__.py +15 -0
- wiz_central_toolkit/readwiz/cli.py +46 -0
- wiz_central_toolkit/readwiz/lib/__init__.py +1 -0
- wiz_central_toolkit/readwiz/lib/cli.py +47 -0
- wiz_central_toolkit/readwiz/lib/read_wizard.py +121 -0
- wiz_central_toolkit/rustwiz/__init__.py +5 -0
- wiz_central_toolkit/rustwiz/__main__.py +7 -0
- wiz_central_toolkit/rustwiz/cli.py +4 -0
- wiz_central_toolkit/rustwiz/lib/__init__.py +1 -0
- wiz_central_toolkit/rustwiz/lib/cli.py +53 -0
- wiz_central_toolkit/rustwiz/lib/rust_wizard.py +128 -0
- wiz_central_toolkit/tmuxwiz/__init__.py +0 -0
- wiz_central_toolkit/tmuxwiz/__main__.py +15 -0
- wiz_central_toolkit/tmuxwiz/cli.py +7 -0
- wiz_central_toolkit/tmuxwiz/lib/__init__.py +1 -0
- wiz_central_toolkit/tmuxwiz/lib/cli.py +10 -0
- wiz_central_toolkit/tmuxwiz/lib/tmux_wizard.py +229 -0
- wiz_central_toolkit/todowiz/__init__.py +0 -0
- wiz_central_toolkit/todowiz/__main__.py +22 -0
- wiz_central_toolkit/todowiz/cli.py +4 -0
- wiz_central_toolkit/todowiz/lib/__init__.py +1 -0
- wiz_central_toolkit/todowiz/lib/cli.py +82 -0
- wiz_central_toolkit/todowiz/lib/todo_wizard.py +134 -0
- wiz_central_toolkit/tools_registry.py +62 -0
- wiz_central_toolkit/wiz/__init__.py +0 -0
- wiz_central_toolkit/wiz/__main__.py +141 -0
- wiz_central_toolkit/writewiz/__init__.py +1 -0
- wiz_central_toolkit/writewiz/__main__.py +9 -0
- wiz_central_toolkit/writewiz/cli.py +264 -0
- wiz_central_toolkit/writewiz/write_wizard.py +384 -0
- wizard_central_toolkit-0.2.0.dist-info/METADATA +203 -0
- wizard_central_toolkit-0.2.0.dist-info/RECORD +77 -0
- wizard_central_toolkit-0.2.0.dist-info/WHEEL +5 -0
- wizard_central_toolkit-0.2.0.dist-info/entry_points.txt +50 -0
- wizard_central_toolkit-0.2.0.dist-info/licenses/LICENSE +201 -0
- wizard_central_toolkit-0.2.0.dist-info/top_level.txt +1 -0
|
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 @@
|
|
|
1
|
+
# wizard package init
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""json-wizard CLI front-end."""
|
|
2
|
+
import sys
|
|
3
|
+
import json as jsonlib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from .json_wizard import JsonWizard
|
|
6
|
+
|
|
7
|
+
def _read_source(arg: str):
|
|
8
|
+
if arg == "-":
|
|
9
|
+
return sys.stdin.read()
|
|
10
|
+
p = Path(arg)
|
|
11
|
+
if not p.exists():
|
|
12
|
+
print(f"[FAIL] file not found: {arg}")
|
|
13
|
+
return None
|
|
14
|
+
return p.read_text()
|
|
15
|
+
|
|
16
|
+
def main(argv=None) -> int:
|
|
17
|
+
args = sys.argv[1:] if argv is None else list(argv)
|
|
18
|
+
if not args or args[0] in ("-h", "--help", "help"):
|
|
19
|
+
print("JSON Wizard — standalone JSON doctor")
|
|
20
|
+
print("alias: jsonwiz")
|
|
21
|
+
print("usage: json_wizard <command> [args]")
|
|
22
|
+
print(" validate <file|-> check JSON validity with line/col on failure")
|
|
23
|
+
print(" pretty <file|-> pretty-print to stdout")
|
|
24
|
+
print(" compact <file|-> minify to one line")
|
|
25
|
+
print(" inspect <file|-> structural summary")
|
|
26
|
+
print(" keys <file|-> top-level keys")
|
|
27
|
+
print(" get <file|-> <path> dotted-path value lookup (a.b.0.c)")
|
|
28
|
+
print(" diff <a> <b> structural diff of two JSON docs")
|
|
29
|
+
print(" stats <file|-> type census: depths, counts")
|
|
30
|
+
print(" merge <a> <b> deep merge (b wins) to stdout")
|
|
31
|
+
return 0
|
|
32
|
+
|
|
33
|
+
cmd = args[0]
|
|
34
|
+
wiz = JsonWizard()
|
|
35
|
+
|
|
36
|
+
if cmd in ("validate", "pretty", "compact", "inspect", "keys", "get", "stats"):
|
|
37
|
+
if len(args) < 2:
|
|
38
|
+
print("out of scope")
|
|
39
|
+
return 2
|
|
40
|
+
source = _read_source(args[1])
|
|
41
|
+
if source is None:
|
|
42
|
+
return 1
|
|
43
|
+
|
|
44
|
+
if cmd == "validate":
|
|
45
|
+
result = wiz.validate(source)
|
|
46
|
+
if result["valid"]:
|
|
47
|
+
print(f"[OK] valid JSON (root type: {result['type']})")
|
|
48
|
+
return 0
|
|
49
|
+
print(f"[FAIL] invalid JSON at line {result['line']}, col {result['col']}: {result['error']}")
|
|
50
|
+
return 1
|
|
51
|
+
if cmd == "pretty":
|
|
52
|
+
try:
|
|
53
|
+
print(wiz.pretty(source))
|
|
54
|
+
return 0
|
|
55
|
+
except jsonlib.JSONDecodeError as e:
|
|
56
|
+
print(f"[FAIL] invalid JSON: {e}")
|
|
57
|
+
return 1
|
|
58
|
+
if cmd == "compact":
|
|
59
|
+
try:
|
|
60
|
+
print(wiz.compact(source))
|
|
61
|
+
return 0
|
|
62
|
+
except jsonlib.JSONDecodeError as e:
|
|
63
|
+
print(f"[FAIL] invalid JSON: {e}")
|
|
64
|
+
return 1
|
|
65
|
+
if cmd == "inspect":
|
|
66
|
+
try:
|
|
67
|
+
print(jsonlib.dumps(wiz.inspect(source), indent=2))
|
|
68
|
+
return 0
|
|
69
|
+
except jsonlib.JSONDecodeError as e:
|
|
70
|
+
print(f"[FAIL] invalid JSON: {e}")
|
|
71
|
+
return 1
|
|
72
|
+
if cmd == "keys":
|
|
73
|
+
try:
|
|
74
|
+
keys = wiz.keys(source)
|
|
75
|
+
print("\n".join(keys) or "[FAIL] root is not an object")
|
|
76
|
+
return 0
|
|
77
|
+
except jsonlib.JSONDecodeError as e:
|
|
78
|
+
print(f"[FAIL] invalid JSON: {e}")
|
|
79
|
+
return 1
|
|
80
|
+
if cmd == "get":
|
|
81
|
+
if len(args) < 3:
|
|
82
|
+
print("out of scope - get requires a path")
|
|
83
|
+
return 2
|
|
84
|
+
try:
|
|
85
|
+
value = wiz.get(source, args[2])
|
|
86
|
+
if isinstance(value, (dict, list)):
|
|
87
|
+
print(jsonlib.dumps(value, indent=2))
|
|
88
|
+
else:
|
|
89
|
+
print(jsonlib.dumps(value))
|
|
90
|
+
return 0
|
|
91
|
+
except (KeyError, IndexError) as e:
|
|
92
|
+
print(f"[FAIL] path not found: {e}")
|
|
93
|
+
return 1
|
|
94
|
+
except jsonlib.JSONDecodeError as e:
|
|
95
|
+
print(f"[FAIL] invalid JSON: {e}")
|
|
96
|
+
return 1
|
|
97
|
+
if cmd == "stats":
|
|
98
|
+
try:
|
|
99
|
+
print(jsonlib.dumps(wiz.stats(source), indent=2))
|
|
100
|
+
return 0
|
|
101
|
+
except jsonlib.JSONDecodeError as e:
|
|
102
|
+
print(f"[FAIL] invalid JSON: {e}")
|
|
103
|
+
return 1
|
|
104
|
+
|
|
105
|
+
if cmd in ("diff", "merge"):
|
|
106
|
+
if len(args) < 3:
|
|
107
|
+
print(f"out of scope - {cmd} requires two files")
|
|
108
|
+
return 2
|
|
109
|
+
sa = _read_source(args[1])
|
|
110
|
+
sb = _read_source(args[2])
|
|
111
|
+
if sa is None or sb is None:
|
|
112
|
+
return 1
|
|
113
|
+
try:
|
|
114
|
+
if cmd == "diff":
|
|
115
|
+
changes = wiz.diff(sa, sb)
|
|
116
|
+
if not changes:
|
|
117
|
+
print("[OK] identical structure and values")
|
|
118
|
+
return 0
|
|
119
|
+
for kind, path, old, new in changes:
|
|
120
|
+
loc = ".".join(str(p) for p in path)
|
|
121
|
+
print(f"[DIFF] {kind} at {loc}")
|
|
122
|
+
if kind == "changed":
|
|
123
|
+
print(f" a: {jsonlib.dumps(old)}")
|
|
124
|
+
print(f" b: {jsonlib.dumps(new)}")
|
|
125
|
+
elif kind == "added":
|
|
126
|
+
print(f" b: {jsonlib.dumps(new)}")
|
|
127
|
+
else:
|
|
128
|
+
print(f" a: {jsonlib.dumps(old)}")
|
|
129
|
+
return 1
|
|
130
|
+
if cmd == "merge":
|
|
131
|
+
print(jsonlib.dumps(wiz.merge(sa, sb), indent=2))
|
|
132
|
+
return 0
|
|
133
|
+
except (jsonlib.JSONDecodeError, TypeError) as e:
|
|
134
|
+
print(f"[FAIL] {e}")
|
|
135
|
+
return 1
|
|
136
|
+
|
|
137
|
+
print("out of scope")
|
|
138
|
+
return 2
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""json-wizard core: standalone, importable JSON doctor. Validate, pretty-print, inspect."""
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
class JsonWizard:
|
|
6
|
+
"""JSON validation and inspection engine. Never modifies the input file."""
|
|
7
|
+
|
|
8
|
+
def validate(self, source: str) -> dict:
|
|
9
|
+
try:
|
|
10
|
+
parsed = json.loads(source)
|
|
11
|
+
except json.JSONDecodeError as e:
|
|
12
|
+
return {"valid": False, "error": str(e), "line": e.lineno, "col": e.colno}
|
|
13
|
+
return {"valid": True, "type": type(parsed).__name__}
|
|
14
|
+
|
|
15
|
+
def pretty(self, source: str, indent: int = 2) -> str:
|
|
16
|
+
return json.dumps(json.loads(source), indent=indent)
|
|
17
|
+
|
|
18
|
+
def inspect(self, source: str) -> dict:
|
|
19
|
+
parsed = json.loads(source)
|
|
20
|
+
|
|
21
|
+
def summarize(obj):
|
|
22
|
+
if isinstance(obj, dict):
|
|
23
|
+
return {k: summarize(v) for k, v in obj.items()}
|
|
24
|
+
if isinstance(obj, list):
|
|
25
|
+
return {"_list_len": len(obj)}
|
|
26
|
+
return obj
|
|
27
|
+
|
|
28
|
+
return summarize(parsed)
|
|
29
|
+
|
|
30
|
+
def keys(self, source: str) -> list:
|
|
31
|
+
parsed = json.loads(source)
|
|
32
|
+
if isinstance(parsed, dict):
|
|
33
|
+
return list(parsed.keys())
|
|
34
|
+
return []
|
|
35
|
+
|
|
36
|
+
def compact(self, source: str) -> str:
|
|
37
|
+
return json.dumps(json.loads(source), separators=(",", ":"))
|
|
38
|
+
|
|
39
|
+
def get(self, source: str, path: str):
|
|
40
|
+
"""Dotted-path lookup. Numeric segments index arrays."""
|
|
41
|
+
node = json.loads(source)
|
|
42
|
+
for seg in path.split("."):
|
|
43
|
+
if isinstance(node, dict) and seg in node:
|
|
44
|
+
node = node[seg]
|
|
45
|
+
elif isinstance(node, list) and seg.lstrip("-").isdigit():
|
|
46
|
+
idx = int(seg)
|
|
47
|
+
if -len(node) <= idx < len(node):
|
|
48
|
+
node = node[idx]
|
|
49
|
+
else:
|
|
50
|
+
raise IndexError(f"index {seg} out of range")
|
|
51
|
+
else:
|
|
52
|
+
raise KeyError(f"path segment not found: {seg}")
|
|
53
|
+
return node
|
|
54
|
+
|
|
55
|
+
def diff(self, source_a: str, source_b: str) -> list:
|
|
56
|
+
"""Structural diff: list of paths where the docs differ."""
|
|
57
|
+
a = json.loads(source_a)
|
|
58
|
+
b = json.loads(source_b)
|
|
59
|
+
changes = []
|
|
60
|
+
|
|
61
|
+
def walk(x, y, path):
|
|
62
|
+
if isinstance(x, dict) and isinstance(y, dict):
|
|
63
|
+
for k in sorted(set(x) | set(y)):
|
|
64
|
+
if k not in x:
|
|
65
|
+
changes.append(("added", path + [k], None, y[k]))
|
|
66
|
+
elif k not in y:
|
|
67
|
+
changes.append(("removed", path + [k], x[k], None))
|
|
68
|
+
else:
|
|
69
|
+
walk(x[k], y[k], path + [k])
|
|
70
|
+
elif isinstance(x, list) and isinstance(y, list):
|
|
71
|
+
for i in range(max(len(x), len(y))):
|
|
72
|
+
if i >= len(x):
|
|
73
|
+
changes.append(("added", path + [i], None, y[i]))
|
|
74
|
+
elif i >= len(y):
|
|
75
|
+
changes.append(("removed", path + [i], x[i], None))
|
|
76
|
+
else:
|
|
77
|
+
walk(x[i], y[i], path + [i])
|
|
78
|
+
elif x != y:
|
|
79
|
+
changes.append(("changed", path, x, y))
|
|
80
|
+
|
|
81
|
+
walk(a, b, [])
|
|
82
|
+
return changes
|
|
83
|
+
|
|
84
|
+
def stats(self, source: str) -> dict:
|
|
85
|
+
parsed = json.loads(source)
|
|
86
|
+
counts = {"dict": 0, "list": 0, "str": 0, "number": 0, "bool": 0, "null": 0}
|
|
87
|
+
|
|
88
|
+
def walk(obj, depth):
|
|
89
|
+
max_d = depth
|
|
90
|
+
if isinstance(obj, dict):
|
|
91
|
+
counts["dict"] += 1
|
|
92
|
+
for v in obj.values():
|
|
93
|
+
max_d = max(max_d, walk(v, depth + 1))
|
|
94
|
+
elif isinstance(obj, list):
|
|
95
|
+
counts["list"] += 1
|
|
96
|
+
for v in obj:
|
|
97
|
+
max_d = max(max_d, walk(v, depth + 1))
|
|
98
|
+
elif isinstance(obj, str):
|
|
99
|
+
counts["str"] += 1
|
|
100
|
+
elif isinstance(obj, bool):
|
|
101
|
+
counts["bool"] += 1
|
|
102
|
+
elif obj is None:
|
|
103
|
+
counts["null"] += 1
|
|
104
|
+
else:
|
|
105
|
+
counts["number"] += 1
|
|
106
|
+
return max_d
|
|
107
|
+
|
|
108
|
+
depth = walk(parsed, 0)
|
|
109
|
+
root_keys = len(parsed) if isinstance(parsed, dict) else None
|
|
110
|
+
return {
|
|
111
|
+
"root_type": type(parsed).__name__,
|
|
112
|
+
"root_keys": root_keys,
|
|
113
|
+
"max_depth": depth,
|
|
114
|
+
**counts,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
def merge(self, source_a: str, source_b: str) -> dict:
|
|
118
|
+
"""Deep merge: b wins on scalars, dicts merge recursively, lists replace."""
|
|
119
|
+
a = json.loads(source_a)
|
|
120
|
+
b = json.loads(source_b)
|
|
121
|
+
if not isinstance(a, dict) or not isinstance(b, dict):
|
|
122
|
+
raise TypeError("merge requires two root objects")
|
|
123
|
+
return self._deep_merge(a, b)
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def _deep_merge(a: dict, b: dict) -> dict:
|
|
127
|
+
out = dict(a)
|
|
128
|
+
for k, v in b.items():
|
|
129
|
+
if isinstance(v, dict) and isinstance(out.get(k), dict):
|
|
130
|
+
out[k] = JsonWizard._deep_merge(out[k], v)
|
|
131
|
+
else:
|
|
132
|
+
out[k] = v
|
|
133
|
+
return out
|
|
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 @@
|
|
|
1
|
+
# wizard package init
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""python-wizard CLI front-end."""
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from .python_wizard import PythonWizard
|
|
6
|
+
|
|
7
|
+
HELP = """Python Wizard — standalone Python doctor (parse only, never executes)
|
|
8
|
+
alias: pythonwiz
|
|
9
|
+
usage: python_wizard <command> <target>
|
|
10
|
+
|
|
11
|
+
check <file|dir> syntax validation (the fleet gate)
|
|
12
|
+
entry <file> shebang + interpreter validation
|
|
13
|
+
outline <file> symbol map: classes/functions + line numbers
|
|
14
|
+
imports <file> stdlib/third_party/local/relative inventory
|
|
15
|
+
todos <file> TODO/FIXME/XXX/BUG/HACK markers
|
|
16
|
+
deadcode <file> defined-but-unreferenced candidates (heuristic)
|
|
17
|
+
complexity <file> branch count per function, hottest first
|
|
18
|
+
loc <file> lines/code/blank/comment/function stats
|
|
19
|
+
docstrings <file> defs missing docstrings
|
|
20
|
+
explain <file> syntax error with source lines shown
|
|
21
|
+
shebangs <dir> sweep tree, validate every shebang
|
|
22
|
+
deps <file> third-party imports as requirements list
|
|
23
|
+
callmap <file> who-calls-whom per function
|
|
24
|
+
longest <file> functions ranked by length
|
|
25
|
+
excepts <file> bare/broad except handlers + swallows
|
|
26
|
+
secrets <file> hardcoded credential patterns (values redacted;
|
|
27
|
+
--show-secrets prints them — a deliberate opt-in)
|
|
28
|
+
mainguard <file> has __main__ guard?
|
|
29
|
+
globals <file> module-level mutable state audit
|
|
30
|
+
magic <file> suspicious numeric literals
|
|
31
|
+
compare <a> <b> structural symbol diff of two files
|
|
32
|
+
tree <dir> directory aggregate summary
|
|
33
|
+
score <file> composite health score 0-100"""
|
|
34
|
+
|
|
35
|
+
def _file(arg: str):
|
|
36
|
+
p = Path(arg)
|
|
37
|
+
if not p.exists():
|
|
38
|
+
print(f"[FAIL] not found: {arg}")
|
|
39
|
+
return None
|
|
40
|
+
return p
|
|
41
|
+
|
|
42
|
+
def main() -> int:
|
|
43
|
+
args = sys.argv[1:]
|
|
44
|
+
if not args or args[0] in ("-h", "--help", "help"):
|
|
45
|
+
print(HELP)
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
cmd = args[0]
|
|
49
|
+
wiz = PythonWizard()
|
|
50
|
+
|
|
51
|
+
# ---- existing ----
|
|
52
|
+
if cmd == "check":
|
|
53
|
+
if len(args) < 2:
|
|
54
|
+
print("out of scope")
|
|
55
|
+
return 2
|
|
56
|
+
target = _file(args[1])
|
|
57
|
+
if target is None:
|
|
58
|
+
return 1
|
|
59
|
+
if target.is_dir():
|
|
60
|
+
results = wiz.check_dir(target)
|
|
61
|
+
if not results:
|
|
62
|
+
print(f"[FAIL] no .py files found under {target}")
|
|
63
|
+
return 1
|
|
64
|
+
bad = 0
|
|
65
|
+
for r in results:
|
|
66
|
+
if r["valid"]:
|
|
67
|
+
print(f"[OK] {r['file']}")
|
|
68
|
+
else:
|
|
69
|
+
bad += 1
|
|
70
|
+
print(f"[FAIL] {r['file']} line {r.get('line')}, col {r.get('col')}: {r['error']}")
|
|
71
|
+
print(f"\n{len(results) - bad}/{len(results)} valid, {bad} invalid")
|
|
72
|
+
return 1 if bad else 0
|
|
73
|
+
r = wiz.check_file(target)
|
|
74
|
+
print(f"[OK] {target}" if r["valid"]
|
|
75
|
+
else f"[FAIL] {target} line {r['line']}, col {r['col']}: {r['error']}")
|
|
76
|
+
return 0 if r["valid"] else 1
|
|
77
|
+
|
|
78
|
+
if cmd == "entry":
|
|
79
|
+
if len(args) < 2:
|
|
80
|
+
print("out of scope")
|
|
81
|
+
return 2
|
|
82
|
+
r = wiz.check_entry(_file(args[1]))
|
|
83
|
+
print(f"[OK] shebang valid: {r.get('interpreter')}" if r["valid"]
|
|
84
|
+
else f"[FAIL] {r['error']}")
|
|
85
|
+
return 0 if r["valid"] else 1
|
|
86
|
+
|
|
87
|
+
# ---- two-file commands ----
|
|
88
|
+
if cmd == "compare":
|
|
89
|
+
if len(args) < 3:
|
|
90
|
+
print("out of scope")
|
|
91
|
+
return 2
|
|
92
|
+
a, b = _file(args[1]), _file(args[2])
|
|
93
|
+
if a is None or b is None:
|
|
94
|
+
return 1
|
|
95
|
+
r = wiz.compare_symbols(a, b)
|
|
96
|
+
if "error" in r:
|
|
97
|
+
print(f"[FAIL] unparseable file: {r['error']}")
|
|
98
|
+
return 1
|
|
99
|
+
if not r["only_a"] and not r["only_b"]:
|
|
100
|
+
print("[OK] identical symbol sets")
|
|
101
|
+
return 0
|
|
102
|
+
for kind, n in r["only_a"]:
|
|
103
|
+
print(f"only in {args[1]}: {kind} {n}")
|
|
104
|
+
for kind, n in r["only_b"]:
|
|
105
|
+
print(f"only in {args[2]}: {kind} {n}")
|
|
106
|
+
print(f"[DIFF] {len(r['only_a'])} only-a, {len(r['only_b'])} only-b, "
|
|
107
|
+
f"{len(r['common'])} common")
|
|
108
|
+
return 1
|
|
109
|
+
|
|
110
|
+
# ---- dir commands ----
|
|
111
|
+
if cmd in ("shebangs", "tree"):
|
|
112
|
+
if len(args) < 2:
|
|
113
|
+
print("out of scope")
|
|
114
|
+
return 2
|
|
115
|
+
d = Path(args[1])
|
|
116
|
+
if not d.is_dir():
|
|
117
|
+
print(f"[FAIL] not a directory: {d}")
|
|
118
|
+
return 1
|
|
119
|
+
if cmd == "shebangs":
|
|
120
|
+
results = wiz.shebangs_dir(d)
|
|
121
|
+
if not results:
|
|
122
|
+
print("[FAIL] no shebang-bearing .py files found")
|
|
123
|
+
return 1
|
|
124
|
+
bad = 0
|
|
125
|
+
for p, interp, ok in results:
|
|
126
|
+
if ok:
|
|
127
|
+
print(f"[OK] {p} -> {interp}")
|
|
128
|
+
else:
|
|
129
|
+
bad += 1
|
|
130
|
+
print(f"[FAIL] {p} -> {interp}")
|
|
131
|
+
print(f"\n{len(results) - bad}/{len(results)} valid, {bad} invalid")
|
|
132
|
+
return 1 if bad else 0
|
|
133
|
+
print(json.dumps(wiz.tree_summary(d), indent=2))
|
|
134
|
+
return 0
|
|
135
|
+
|
|
136
|
+
# ---- single-file commands ----
|
|
137
|
+
single = ("outline", "imports", "todos", "deadcode", "complexity", "loc",
|
|
138
|
+
"docstrings", "explain", "deps", "callmap", "longest",
|
|
139
|
+
"excepts", "secrets", "mainguard", "globals", "magic", "score")
|
|
140
|
+
if cmd in single:
|
|
141
|
+
show_secrets = False
|
|
142
|
+
if cmd == "secrets" and "--show-secrets" in args[1:]:
|
|
143
|
+
# Deliberate opt-in (AVS ruling, 2026-11-09): the flag is
|
|
144
|
+
# stripped wherever it sits so the file stays at args[1].
|
|
145
|
+
show_secrets = True
|
|
146
|
+
args = [args[0]] + [a for a in args[1:] if a != "--show-secrets"]
|
|
147
|
+
if len(args) < 2:
|
|
148
|
+
print("out of scope")
|
|
149
|
+
return 2
|
|
150
|
+
p = _file(args[1])
|
|
151
|
+
if p is None:
|
|
152
|
+
return 1
|
|
153
|
+
|
|
154
|
+
if cmd == "outline":
|
|
155
|
+
rows = wiz.outline(p)
|
|
156
|
+
print("\n".join(f"L{ln:4d} {kind:5s} {name}"
|
|
157
|
+
for kind, name, ln in rows) or "[FAIL] no symbols")
|
|
158
|
+
return 0
|
|
159
|
+
if cmd == "imports":
|
|
160
|
+
print(json.dumps(wiz.imports_of(p), indent=2))
|
|
161
|
+
return 0
|
|
162
|
+
if cmd == "todos":
|
|
163
|
+
rows = wiz.todos_of(p)
|
|
164
|
+
print("\n".join(f"L{ln:4d} [{tag}] {msg}" for ln, tag, msg in rows)
|
|
165
|
+
or "[OK] no markers found")
|
|
166
|
+
return 1 if rows else 0
|
|
167
|
+
if cmd == "deadcode":
|
|
168
|
+
rows = wiz.deadcode(p)
|
|
169
|
+
print("[hint] candidates only — attribute-called methods may false-positive")
|
|
170
|
+
print("\n".join(f"L{ln:4d} {name}" for name, ln in rows)
|
|
171
|
+
or "[OK] no dead code candidates")
|
|
172
|
+
return 1 if rows else 0
|
|
173
|
+
if cmd == "complexity":
|
|
174
|
+
print("\n".join(f"{c:3d} branches L{ln:4d} {name}"
|
|
175
|
+
for name, ln, c in wiz.complexity(p))
|
|
176
|
+
or "[FAIL] no functions")
|
|
177
|
+
return 0
|
|
178
|
+
if cmd == "loc":
|
|
179
|
+
print(json.dumps(wiz.loc_stats(p), indent=2))
|
|
180
|
+
return 0
|
|
181
|
+
if cmd == "docstrings":
|
|
182
|
+
rows = wiz.docstring_report(p)
|
|
183
|
+
print("\n".join(f"L{ln:4d} {name} — no docstring" for ln, name in rows)
|
|
184
|
+
or "[OK] all documented")
|
|
185
|
+
return 1 if rows else 0
|
|
186
|
+
if cmd == "explain":
|
|
187
|
+
r = wiz.explain_error(p)
|
|
188
|
+
if r["valid"]:
|
|
189
|
+
print(f"[OK] {p} parses clean — nothing to explain")
|
|
190
|
+
return 0
|
|
191
|
+
print(f"[FAIL] {p} line {r['line']}, col {r['col']}: {r['error']}")
|
|
192
|
+
print("\n".join(r["context"]))
|
|
193
|
+
return 1
|
|
194
|
+
if cmd == "deps":
|
|
195
|
+
deps = wiz.deps(p)
|
|
196
|
+
print("\n".join(deps) or "[OK] stdlib-only — zero third-party deps")
|
|
197
|
+
return 0
|
|
198
|
+
if cmd == "callmap":
|
|
199
|
+
print(json.dumps(wiz.call_map(p), indent=2))
|
|
200
|
+
return 0
|
|
201
|
+
if cmd == "longest":
|
|
202
|
+
print("\n".join(f"{length:4d} lines L{ln:4d} {name}"
|
|
203
|
+
for name, ln, length in wiz.longest_functions(p))
|
|
204
|
+
or "[FAIL] no functions")
|
|
205
|
+
return 0
|
|
206
|
+
if cmd == "excepts":
|
|
207
|
+
rows = wiz.bare_excepts(p)
|
|
208
|
+
print("\n".join(f"L{ln:4d} {kind} except{' + swallow' if swallow else ''}"
|
|
209
|
+
for ln, kind, swallow in rows)
|
|
210
|
+
or "[OK] no bare/broad excepts")
|
|
211
|
+
return 1 if rows else 0
|
|
212
|
+
if cmd == "secrets":
|
|
213
|
+
rows = wiz.secrets_scan(p, show_secrets=show_secrets)
|
|
214
|
+
if show_secrets:
|
|
215
|
+
# Exactly the pre-ruling full-line behaviour.
|
|
216
|
+
print("\n".join(f"L{ln:4d} {line}" for ln, _kind, line in rows)
|
|
217
|
+
or "[OK] no credential patterns found")
|
|
218
|
+
else:
|
|
219
|
+
print("\n".join(f"L{ln:4d} [{kind}] {line}"
|
|
220
|
+
for ln, kind, line in rows)
|
|
221
|
+
or "[OK] no credential patterns found")
|
|
222
|
+
return 1 if rows else 0
|
|
223
|
+
if cmd == "mainguard":
|
|
224
|
+
r = wiz.main_guard(p)
|
|
225
|
+
print("[OK] __main__ guard present" if r["has_main_guard"]
|
|
226
|
+
else "[FAIL] no __main__ guard")
|
|
227
|
+
return 0 if r["has_main_guard"] else 1
|
|
228
|
+
if cmd == "globals":
|
|
229
|
+
rows = wiz.globals_audit(p)
|
|
230
|
+
print("\n".join(f"L{ln:4d} {names} ({mut})" for ln, names, mut in rows)
|
|
231
|
+
or "[OK] no module-level assignments")
|
|
232
|
+
mutables = [r for r in rows if r[2] == "mutable"]
|
|
233
|
+
return 1 if mutables else 0
|
|
234
|
+
if cmd == "magic":
|
|
235
|
+
rows = wiz.magic_numbers(p)
|
|
236
|
+
print("\n".join(f"L{ln:4d} {val}" for ln, val in rows)
|
|
237
|
+
or "[OK] no suspicious literals")
|
|
238
|
+
return 0
|
|
239
|
+
if cmd == "score":
|
|
240
|
+
print(json.dumps(wiz.health_score(p), indent=2))
|
|
241
|
+
return 0
|
|
242
|
+
|
|
243
|
+
print("out of scope")
|
|
244
|
+
return 2
|
|
245
|
+
|
|
246
|
+
if __name__ == "__main__":
|
|
247
|
+
sys.exit(main())
|