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
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Shared tool registry — both `wiz` and `godfatherwiz` dispatch through this.
|
|
2
|
+
Add a new tool by adding one line here; nothing else needs to know about it.
|
|
3
|
+
|
|
4
|
+
Name law (AVS, 2026-11-09): a wizard's name-command is its full
|
|
5
|
+
underscored form (`godfather_wizard`); the short form (`godfatherwiz`)
|
|
6
|
+
is an alias — installed, working, and listed as an alias, never the
|
|
7
|
+
name. CITIZENS is keyed by the full form; TOOLS resolves both
|
|
8
|
+
spellings to the same main.
|
|
9
|
+
"""
|
|
10
|
+
import importlib
|
|
11
|
+
|
|
12
|
+
CITIZENS = {
|
|
13
|
+
"banner_wizard": "wiz_central_toolkit.bannerwiz.__main__",
|
|
14
|
+
"codeguard_wizard": "wiz_central_toolkit.codeguardwiz.__main__",
|
|
15
|
+
"debug_wizard": "wiz_central_toolkit.debugwiz.__main__",
|
|
16
|
+
"godfather_wizard": "wiz_central_toolkit.godfatherwiz.__main__",
|
|
17
|
+
"diff_wizard": "wiz_central_toolkit.diffwiz.__main__",
|
|
18
|
+
"json_wizard": "wiz_central_toolkit.jsonwiz.__main__",
|
|
19
|
+
"python_wizard": "wiz_central_toolkit.pythonwiz.__main__",
|
|
20
|
+
"read_wizard": "wiz_central_toolkit.readwiz.__main__",
|
|
21
|
+
"rust_wizard": "wiz_central_toolkit.rustwiz.__main__",
|
|
22
|
+
"tmux_wizard": "wiz_central_toolkit.tmuxwiz.__main__",
|
|
23
|
+
"todo_wizard": "wiz_central_toolkit.todowiz.__main__",
|
|
24
|
+
"write_wizard": "wiz_central_toolkit.writewiz.__main__",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
ALIAS_OF = {
|
|
28
|
+
"bannerwiz": "banner_wizard",
|
|
29
|
+
"codeguardwiz": "codeguard_wizard",
|
|
30
|
+
"debugwiz": "debug_wizard",
|
|
31
|
+
"godfatherwiz": "godfather_wizard",
|
|
32
|
+
"diffwiz": "diff_wizard",
|
|
33
|
+
"jsonwiz": "json_wizard",
|
|
34
|
+
"pythonwiz": "python_wizard",
|
|
35
|
+
"readwiz": "read_wizard",
|
|
36
|
+
"rustwiz": "rust_wizard",
|
|
37
|
+
"tmuxwiz": "tmux_wizard",
|
|
38
|
+
"todowiz": "todo_wizard",
|
|
39
|
+
"writewiz": "write_wizard",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def full_form(name: str):
|
|
44
|
+
"""The wizard's name-command for either spelling, else None."""
|
|
45
|
+
if name in CITIZENS:
|
|
46
|
+
return name
|
|
47
|
+
return ALIAS_OF.get(name)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Both spellings resolve — dual activation, one main per citizen.
|
|
51
|
+
TOOLS = dict(CITIZENS)
|
|
52
|
+
TOOLS.update({alias: CITIZENS[full] for alias, full in ALIAS_OF.items()})
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_tool_main(name: str):
|
|
56
|
+
"""Import a tool's main() lazily by name (either spelling).
|
|
57
|
+
Returns None if unknown."""
|
|
58
|
+
module_path = TOOLS.get(name)
|
|
59
|
+
if module_path is None:
|
|
60
|
+
return None
|
|
61
|
+
module = importlib.import_module(module_path)
|
|
62
|
+
return module.main
|
|
File without changes
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""The Wizarding Router — top-level dispatcher for Wizard Central Toolkit.
|
|
2
|
+
|
|
3
|
+
The router's full name is the Wizarding Router (AVS, 2026-11-09);
|
|
4
|
+
its command is `wiz`, and the command does not change.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import contextlib
|
|
10
|
+
import importlib.metadata
|
|
11
|
+
import io
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
from wiz_central_toolkit.tools_registry import (
|
|
16
|
+
ALIAS_OF,
|
|
17
|
+
CITIZENS,
|
|
18
|
+
full_form,
|
|
19
|
+
get_tool_main,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Router stem aliases — the shortest typing for each citizen.
|
|
23
|
+
ALIASES = {
|
|
24
|
+
"banner": "banner_wizard",
|
|
25
|
+
"codeguard": "codeguard_wizard",
|
|
26
|
+
"debug": "debug_wizard",
|
|
27
|
+
"diff": "diff_wizard",
|
|
28
|
+
"json": "json_wizard",
|
|
29
|
+
"python": "python_wizard",
|
|
30
|
+
"read": "read_wizard",
|
|
31
|
+
"rust": "rust_wizard",
|
|
32
|
+
"tmux": "tmux_wizard",
|
|
33
|
+
"todo": "todo_wizard",
|
|
34
|
+
"write": "write_wizard",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def package_version() -> str:
|
|
39
|
+
for dist_name in ("wizard-central-toolkit", "wiz-central-toolkit"):
|
|
40
|
+
try:
|
|
41
|
+
return importlib.metadata.version(dist_name)
|
|
42
|
+
except importlib.metadata.PackageNotFoundError:
|
|
43
|
+
continue
|
|
44
|
+
return "0.2.0"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def print_help() -> None:
|
|
48
|
+
print("The Wizarding Router — the fleet's dispatcher")
|
|
49
|
+
print("usage: wiz [--json] <tool> [args...]")
|
|
50
|
+
print()
|
|
51
|
+
print("options:")
|
|
52
|
+
print(" -h, --help show this help message")
|
|
53
|
+
print(" --version show the installed version")
|
|
54
|
+
print(" --json emit command output as JSON")
|
|
55
|
+
print()
|
|
56
|
+
print("available tools: " + ", ".join(sorted(CITIZENS)))
|
|
57
|
+
print()
|
|
58
|
+
print("aliases (short forms — the same wizards, shorter typing):")
|
|
59
|
+
for alias, tool in sorted(ALIAS_OF.items()):
|
|
60
|
+
print(f" {alias:<12} alias of {tool}")
|
|
61
|
+
print()
|
|
62
|
+
print("router aliases:")
|
|
63
|
+
for alias, tool in sorted(ALIASES.items()):
|
|
64
|
+
print(f" {alias:<12} {tool}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def normalize_tool_name(name: str) -> str:
|
|
68
|
+
"""Canonical name-command for a tool reference: a router stem,
|
|
69
|
+
a short alias, or the full form itself all name the full form."""
|
|
70
|
+
if name in ALIASES:
|
|
71
|
+
return ALIASES[name]
|
|
72
|
+
return full_form(name) or name
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def run_json(tool_name: str, tool_args: list[str], tool_main) -> int:
|
|
76
|
+
stdout_buffer = io.StringIO()
|
|
77
|
+
stderr_buffer = io.StringIO()
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
with (
|
|
81
|
+
contextlib.redirect_stdout(stdout_buffer),
|
|
82
|
+
contextlib.redirect_stderr(stderr_buffer),
|
|
83
|
+
):
|
|
84
|
+
result = tool_main(tool_args)
|
|
85
|
+
|
|
86
|
+
exit_code = result if isinstance(result, int) else 0
|
|
87
|
+
except SystemExit as exc:
|
|
88
|
+
exit_code = exc.code if isinstance(exc.code, int) else 1
|
|
89
|
+
except Exception as exc:
|
|
90
|
+
exit_code = 1
|
|
91
|
+
stderr_buffer.write(f"{type(exc).__name__}: {exc}")
|
|
92
|
+
|
|
93
|
+
payload = {
|
|
94
|
+
"tool": tool_name,
|
|
95
|
+
"args": tool_args,
|
|
96
|
+
"exit_code": exit_code,
|
|
97
|
+
"stdout": stdout_buffer.getvalue(),
|
|
98
|
+
"stderr": stderr_buffer.getvalue(),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
print(json.dumps(payload, indent=2))
|
|
102
|
+
return exit_code
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def main(argv=None) -> int:
|
|
106
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
107
|
+
|
|
108
|
+
if not argv or argv[0] in {"-h", "--help", "help"}:
|
|
109
|
+
print_help()
|
|
110
|
+
return 0
|
|
111
|
+
|
|
112
|
+
if argv[0] in {"--version", "-V"}:
|
|
113
|
+
print(f"wiz {package_version()}")
|
|
114
|
+
return 0
|
|
115
|
+
|
|
116
|
+
json_mode = "--json" in argv
|
|
117
|
+
if json_mode:
|
|
118
|
+
argv = [argument for argument in argv if argument != "--json"]
|
|
119
|
+
|
|
120
|
+
if not argv:
|
|
121
|
+
print_help()
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
requested_name, tool_args = argv[0], argv[1:]
|
|
125
|
+
tool_name = normalize_tool_name(requested_name)
|
|
126
|
+
tool_main = get_tool_main(tool_name)
|
|
127
|
+
|
|
128
|
+
if tool_main is None:
|
|
129
|
+
print(f"unknown tool: {requested_name!r}", file=sys.stderr)
|
|
130
|
+
print("available tools: " + ", ".join(sorted(CITIZENS)), file=sys.stderr)
|
|
131
|
+
return 2
|
|
132
|
+
|
|
133
|
+
if json_mode:
|
|
134
|
+
return run_json(tool_name, tool_args, tool_main)
|
|
135
|
+
|
|
136
|
+
result = tool_main(tool_args)
|
|
137
|
+
return result if isinstance(result, int) else 0
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == "__main__":
|
|
141
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""writewiz — part of Wizard Central Toolkit."""
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""write-wizard face — thin CLI over the consent wall."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from .write_wizard import (
|
|
7
|
+
BACKUPS,
|
|
8
|
+
LAW_TEXT,
|
|
9
|
+
QUARANTINE,
|
|
10
|
+
_draft_path,
|
|
11
|
+
_ensure_quarantine,
|
|
12
|
+
audit_quarantine,
|
|
13
|
+
commit,
|
|
14
|
+
discard,
|
|
15
|
+
explain,
|
|
16
|
+
gate_and_diff,
|
|
17
|
+
guard_sweep,
|
|
18
|
+
law_digest,
|
|
19
|
+
ledger_rows,
|
|
20
|
+
load_protected,
|
|
21
|
+
propose,
|
|
22
|
+
protect,
|
|
23
|
+
purge_old,
|
|
24
|
+
rollback,
|
|
25
|
+
stats,
|
|
26
|
+
unprotect,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
USAGE = """Write Wizard — the consent wall for file writes
|
|
30
|
+
alias: writewiz
|
|
31
|
+
usage: write_wizard propose <path> stage content (reads stdin) in quarantine
|
|
32
|
+
write_wizard preview <path> gate + diff the staged draft vs disk
|
|
33
|
+
write_wizard commit <path> ask Governor consent, write, verify
|
|
34
|
+
write_wizard pending list quarantined drafts
|
|
35
|
+
write_wizard law print the WRITE LAW alone
|
|
36
|
+
write_wizard status <path> draft state: bytes, age, target
|
|
37
|
+
write_wizard diff <path> disk vs draft diff alone
|
|
38
|
+
write_wizard verify <path> gate + verification, zero consent
|
|
39
|
+
write_wizard explain <path> would-commit verdict with reasons
|
|
40
|
+
write_wizard discard <path> burn a draft ('y'-gated, journaled)
|
|
41
|
+
write_wizard guard gate sweep over every pending draft
|
|
42
|
+
write_wizard audit quarantine census: drafts, orphans
|
|
43
|
+
write_wizard ledger full write journal
|
|
44
|
+
write_wizard history <path> journal filtered to one target
|
|
45
|
+
write_wizard stats counts: staged, written, refused
|
|
46
|
+
write_wizard protect <path> forbid writes — absolute refusal
|
|
47
|
+
write_wizard unprotect <path> lift a protection (Governor only)
|
|
48
|
+
write_wizard protected list protected paths
|
|
49
|
+
write_wizard backups list pre-write safety copies
|
|
50
|
+
write_wizard rollback <path> restore from backup ('y'-gated)
|
|
51
|
+
write_wizard purge-old <days> burn drafts older than N days ('y'-gated)
|
|
52
|
+
write_wizard law-digest sha256 of WRITE LAW (drift detection)
|
|
53
|
+
exit codes: 0 ok | 1 refused/failed | 2 usage error"""
|
|
54
|
+
|
|
55
|
+
CMDS = ("propose", "preview", "commit", "pending", "law", "status", "diff",
|
|
56
|
+
"verify", "explain", "discard", "guard", "audit", "ledger", "history",
|
|
57
|
+
"stats", "protect", "unprotect", "protected", "backups", "rollback",
|
|
58
|
+
"purge-old", "law-digest")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _load(target_str):
|
|
62
|
+
from pathlib import Path
|
|
63
|
+
_ensure_quarantine()
|
|
64
|
+
draft, meta = _draft_path(Path(target_str).expanduser())
|
|
65
|
+
if not draft.exists() or not meta.exists():
|
|
66
|
+
print(f"[FAIL] no quarantined draft for {target_str} — propose first")
|
|
67
|
+
return None
|
|
68
|
+
return draft, meta
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def main(argv=None):
|
|
72
|
+
import datetime as dt
|
|
73
|
+
from pathlib import Path
|
|
74
|
+
|
|
75
|
+
args = sys.argv[1:] if argv is None else list(argv)
|
|
76
|
+
if args and args[0] in ("-h", "--help", "help"):
|
|
77
|
+
print(USAGE)
|
|
78
|
+
return 0
|
|
79
|
+
if not args or args[0] not in CMDS:
|
|
80
|
+
print(USAGE)
|
|
81
|
+
return 2
|
|
82
|
+
|
|
83
|
+
cmd = args[0]
|
|
84
|
+
|
|
85
|
+
if cmd == "law":
|
|
86
|
+
print(LAW_TEXT)
|
|
87
|
+
return 0
|
|
88
|
+
if cmd == "law-digest":
|
|
89
|
+
print(json.dumps(law_digest(), indent=2))
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
if cmd == "pending":
|
|
93
|
+
_ensure_quarantine()
|
|
94
|
+
drafts = sorted(QUARANTINE.glob("*.draft"))
|
|
95
|
+
if not drafts:
|
|
96
|
+
print("(quarantine empty)")
|
|
97
|
+
return 0
|
|
98
|
+
for d in drafts:
|
|
99
|
+
meta = d.with_suffix(".meta")
|
|
100
|
+
target = meta.read_text(encoding="utf-8") if meta.exists() else "?"
|
|
101
|
+
print(f" {d.name} -> {target}")
|
|
102
|
+
return 0
|
|
103
|
+
if cmd == "guard":
|
|
104
|
+
rows = guard_sweep()
|
|
105
|
+
if not rows:
|
|
106
|
+
print("(quarantine empty)")
|
|
107
|
+
return 0
|
|
108
|
+
rc = 0
|
|
109
|
+
for row in rows:
|
|
110
|
+
mark = "[OK]" if row["gate"] in ("PASS", "waived") else "[FAIL]"
|
|
111
|
+
if row["gate"] == "FAIL":
|
|
112
|
+
rc = 1
|
|
113
|
+
print(f"{mark} {row['draft']} gate={row['gate']} -> {row['target']}")
|
|
114
|
+
return rc
|
|
115
|
+
if cmd == "audit":
|
|
116
|
+
r = audit_quarantine()
|
|
117
|
+
print(f"drafts={r['drafts']} orphans={len(r['orphans'])} "
|
|
118
|
+
f"ghosts={len(r['ghosts'])} oldest={r['oldest'] or 'never'}")
|
|
119
|
+
for o in r["orphans"]:
|
|
120
|
+
print(f"[ORPHAN] {o}")
|
|
121
|
+
for g in r["ghosts"]:
|
|
122
|
+
print(f"[GHOST] {g}")
|
|
123
|
+
return 1 if (r["orphans"] or r["ghosts"]) else 0
|
|
124
|
+
if cmd == "ledger":
|
|
125
|
+
rows = ledger_rows()
|
|
126
|
+
if not rows:
|
|
127
|
+
print("(ledger empty — no wall events yet)")
|
|
128
|
+
return 0
|
|
129
|
+
for row in rows:
|
|
130
|
+
print(f"{row['at']} {row['event']:16s} {row['target']} {row['detail']}")
|
|
131
|
+
return 0
|
|
132
|
+
if cmd == "stats":
|
|
133
|
+
print(json.dumps(stats(), indent=2))
|
|
134
|
+
return 0
|
|
135
|
+
if cmd == "protected":
|
|
136
|
+
prot = load_protected()
|
|
137
|
+
if not prot:
|
|
138
|
+
print("(no protected paths)")
|
|
139
|
+
return 0
|
|
140
|
+
for p in prot:
|
|
141
|
+
print(f"[SHIELD] {p}")
|
|
142
|
+
return 0
|
|
143
|
+
if cmd == "backups":
|
|
144
|
+
_ensure_quarantine()
|
|
145
|
+
baks = sorted(BACKUPS.glob("*.bak"))
|
|
146
|
+
if not baks:
|
|
147
|
+
print("(no backups — nothing has been overwritten yet)")
|
|
148
|
+
return 0
|
|
149
|
+
for b in baks:
|
|
150
|
+
print(f"[BAK] {b.name} ({b.stat().st_size} bytes, "
|
|
151
|
+
f"{dt.datetime.fromtimestamp(b.stat().st_mtime).isoformat(timespec='seconds')})")
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
if cmd in ("protect", "unprotect") :
|
|
155
|
+
if len(args) < 2:
|
|
156
|
+
print("[FAIL] requires a path")
|
|
157
|
+
return 2
|
|
158
|
+
r = protect(args[1]) if cmd == "protect" else unprotect(args[1])
|
|
159
|
+
if r["ok"]:
|
|
160
|
+
print(f"[OK] {cmd}ed: {list(r.values())[1]}")
|
|
161
|
+
return 0
|
|
162
|
+
print(f"[FAIL] {r['error']}")
|
|
163
|
+
return 1
|
|
164
|
+
|
|
165
|
+
if cmd == "purge-old":
|
|
166
|
+
if len(args) < 2:
|
|
167
|
+
print("[FAIL] purge-old requires a number of days")
|
|
168
|
+
return 2
|
|
169
|
+
ok, msg, _ = purge_old(int(args[1]))
|
|
170
|
+
print(msg)
|
|
171
|
+
return 0 if ok else 1
|
|
172
|
+
|
|
173
|
+
# --- path-taking commands ---
|
|
174
|
+
if len(args) < 2:
|
|
175
|
+
print(f"[FAIL] writewiz {cmd} requires a path")
|
|
176
|
+
return 2
|
|
177
|
+
target_str = args[1]
|
|
178
|
+
target = Path(target_str).expanduser()
|
|
179
|
+
|
|
180
|
+
if cmd == "propose":
|
|
181
|
+
content = sys.stdin.read()
|
|
182
|
+
draft, meta, t = propose(target_str, content)
|
|
183
|
+
print(f"[OK] staged: {draft.name} -> {t} ({len(content)} chars quarantined)")
|
|
184
|
+
return 0
|
|
185
|
+
if cmd == "status":
|
|
186
|
+
loaded = _load(target_str)
|
|
187
|
+
if loaded is None:
|
|
188
|
+
return 1
|
|
189
|
+
draft, meta = loaded
|
|
190
|
+
age = dt.datetime.now() - dt.datetime.fromtimestamp(draft.stat().st_mtime)
|
|
191
|
+
print(f"target: {meta.read_text(encoding='utf-8').strip()}")
|
|
192
|
+
print(f"draft: {draft.name}")
|
|
193
|
+
print(f"bytes: {draft.stat().st_size}")
|
|
194
|
+
print(f"staged: {age.seconds // 60} minutes ago")
|
|
195
|
+
print(f"shielded: {'YES — writes refused' if (lambda tw: tw.resolve().as_posix() in [p for p in load_protected()] or str(tw) in load_protected())(target) else 'no'}")
|
|
196
|
+
return 0
|
|
197
|
+
if cmd == "diff":
|
|
198
|
+
loaded = _load(target_str)
|
|
199
|
+
if loaded is None:
|
|
200
|
+
return 1
|
|
201
|
+
draft, meta = loaded
|
|
202
|
+
report, hard_fail = gate_and_diff(draft, target)
|
|
203
|
+
for line in report:
|
|
204
|
+
print(line)
|
|
205
|
+
return 1 if hard_fail else 0
|
|
206
|
+
if cmd == "verify":
|
|
207
|
+
loaded = _load(target_str)
|
|
208
|
+
if loaded is None:
|
|
209
|
+
return 1
|
|
210
|
+
draft, meta = loaded
|
|
211
|
+
if target.exists():
|
|
212
|
+
on_disk = target.read_text(encoding="utf-8", errors="replace")
|
|
213
|
+
draft_text = draft.read_text(encoding="utf-8", errors="replace")
|
|
214
|
+
print("[OK] draft matches disk" if on_disk == draft_text
|
|
215
|
+
else "[FAIL] draft differs from disk")
|
|
216
|
+
return 0 if on_disk == draft_text else 1
|
|
217
|
+
print("[NOTE] target does not exist yet — new file")
|
|
218
|
+
return 0
|
|
219
|
+
if cmd == "explain":
|
|
220
|
+
r = explain(target_str)
|
|
221
|
+
if not r["ok"]:
|
|
222
|
+
print(f"[FAIL] {r['error']}")
|
|
223
|
+
return 1
|
|
224
|
+
verdict = "WOULD WRITE" if r["would_write"] else "WOULD REFUSE"
|
|
225
|
+
print(f"[{verdict}] {r['target']} ({r['draft_bytes']} bytes staged)")
|
|
226
|
+
for reason in r["reasons"]:
|
|
227
|
+
print(f" - {reason}")
|
|
228
|
+
return 0
|
|
229
|
+
if cmd == "preview":
|
|
230
|
+
loaded = _load(target_str)
|
|
231
|
+
if loaded is None:
|
|
232
|
+
return 1
|
|
233
|
+
draft, meta = loaded
|
|
234
|
+
report, hard_fail = gate_and_diff(draft, target)
|
|
235
|
+
for line in report:
|
|
236
|
+
print(line)
|
|
237
|
+
return 1 if hard_fail else 0
|
|
238
|
+
if cmd == "commit":
|
|
239
|
+
loaded = _load(target_str)
|
|
240
|
+
if loaded is None:
|
|
241
|
+
return 1
|
|
242
|
+
draft, meta = loaded
|
|
243
|
+
ok, msg, t = commit(draft, meta)
|
|
244
|
+
print(msg)
|
|
245
|
+
return 0 if ok else 1
|
|
246
|
+
if cmd == "history":
|
|
247
|
+
rows = [r for r in ledger_rows() if target.as_posix() in r["target"]]
|
|
248
|
+
if not rows:
|
|
249
|
+
print(f"(no journal entries for {target_str})")
|
|
250
|
+
return 0
|
|
251
|
+
for row in rows:
|
|
252
|
+
print(f"{row['at']} {row['event']:16s} {row['detail']}")
|
|
253
|
+
return 0
|
|
254
|
+
if cmd == "discard":
|
|
255
|
+
ok, msg, t = discard(target_str)
|
|
256
|
+
print(msg)
|
|
257
|
+
return 0 if ok else 1
|
|
258
|
+
if cmd == "rollback":
|
|
259
|
+
ok, msg, t = rollback(target_str)
|
|
260
|
+
print(msg)
|
|
261
|
+
return 0 if ok else 1
|
|
262
|
+
|
|
263
|
+
print(USAGE)
|
|
264
|
+
return 2
|