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,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,9 @@
1
+ """Entry point for writewiz."""
2
+ import sys
3
+ from .cli import main
4
+
5
+ # Expose main() at module level
6
+ main = main
7
+
8
+ if __name__ == "__main__":
9
+ sys.exit(main())
@@ -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