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,128 @@
|
|
|
1
|
+
"""rust-wizard: honest Rust validator. No tracebacks, verdicts only."""
|
|
2
|
+
import subprocess
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
LAW_TEXT = """RUST LAW (Rust Wizard, the honest validator)
|
|
7
|
+
1. The Rust Wizard judges Rust; it never writes it. Verdicts only.
|
|
8
|
+
2. Every verdict comes from the real toolchain (rustc, cargo).
|
|
9
|
+
No toolchain, no verdict: a plain message, exit 2.
|
|
10
|
+
3. fmt speaks in --check: it reports drift, it never reformats.
|
|
11
|
+
4. clean is the one delete the Rust Wizard performs: a crate's
|
|
12
|
+
own target/ directory, and nothing else, ever.
|
|
13
|
+
5. Mortal and stateless: one command, one verdict, then death.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def package_version():
|
|
18
|
+
try:
|
|
19
|
+
from importlib import metadata
|
|
20
|
+
for _n in ("wizard-central-toolkit", "wiz-central-toolkit"):
|
|
21
|
+
try:
|
|
22
|
+
return metadata.version(_n)
|
|
23
|
+
except metadata.PackageNotFoundError:
|
|
24
|
+
continue
|
|
25
|
+
except Exception:
|
|
26
|
+
pass
|
|
27
|
+
try:
|
|
28
|
+
from wiz_central_toolkit import __version__
|
|
29
|
+
return __version__
|
|
30
|
+
except Exception:
|
|
31
|
+
return "unknown"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RustWizard:
|
|
35
|
+
NAME = "rustwiz"
|
|
36
|
+
|
|
37
|
+
def check(self, targets):
|
|
38
|
+
files = []
|
|
39
|
+
for target in targets:
|
|
40
|
+
t = Path(target).expanduser()
|
|
41
|
+
if t.is_dir():
|
|
42
|
+
files.extend(sorted(t.rglob("*.rs")))
|
|
43
|
+
elif t.suffix == ".rs" and t.is_file():
|
|
44
|
+
files.append(t)
|
|
45
|
+
else:
|
|
46
|
+
print(f"out of scope - not a .rs file or directory: {target}")
|
|
47
|
+
return 2
|
|
48
|
+
if not files:
|
|
49
|
+
print("[FAIL] no .rs files found")
|
|
50
|
+
return 1
|
|
51
|
+
bad = 0
|
|
52
|
+
for f in files:
|
|
53
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
54
|
+
try:
|
|
55
|
+
r = subprocess.run(
|
|
56
|
+
["rustc", "--edition", "2021", "-A", "warnings",
|
|
57
|
+
"--crate-type", "lib", "--emit=metadata",
|
|
58
|
+
"--out-dir", tmp, str(f)],
|
|
59
|
+
capture_output=True, text=True, timeout=120)
|
|
60
|
+
except (FileNotFoundError, OSError):
|
|
61
|
+
print("[FAIL] rustc not found — install Rust to use rust_wizard check")
|
|
62
|
+
return 2
|
|
63
|
+
if r.returncode == 0:
|
|
64
|
+
print(f"[OK] {f}")
|
|
65
|
+
else:
|
|
66
|
+
bad += 1
|
|
67
|
+
print(f"[FAIL] {f}")
|
|
68
|
+
first = (r.stderr or "").strip().splitlines()
|
|
69
|
+
for line in first[:5]:
|
|
70
|
+
print(f" {line}")
|
|
71
|
+
print(f"{len(files) - bad}/{len(files)} valid, {bad} invalid")
|
|
72
|
+
return 1 if bad else 0
|
|
73
|
+
|
|
74
|
+
def _cargo(self, crate_dir, args, verb, show_stdout=False):
|
|
75
|
+
"""Run one cargo verb against a crate. One verdict shape for all."""
|
|
76
|
+
d = Path(crate_dir).expanduser()
|
|
77
|
+
if not (d / "Cargo.toml").is_file():
|
|
78
|
+
print(f"[FAIL] no Cargo.toml in {d}")
|
|
79
|
+
return 1
|
|
80
|
+
try:
|
|
81
|
+
r = subprocess.run(["cargo"] + args, cwd=str(d),
|
|
82
|
+
capture_output=True, text=True, timeout=600)
|
|
83
|
+
except (FileNotFoundError, OSError):
|
|
84
|
+
print(f"[FAIL] cargo not found — install Rust to use rust_wizard {verb}")
|
|
85
|
+
return 2
|
|
86
|
+
if r.returncode == 0:
|
|
87
|
+
print(f"[OK] cargo {args[0]} passed: {d}")
|
|
88
|
+
return 0
|
|
89
|
+
print(f"[FAIL] cargo {args[0]} failed: {d}")
|
|
90
|
+
stream = (r.stdout if show_stdout else r.stderr) or ""
|
|
91
|
+
for line in stream.strip().splitlines()[:8]:
|
|
92
|
+
print(f" {line}")
|
|
93
|
+
return 1
|
|
94
|
+
|
|
95
|
+
def build(self, crate_dir):
|
|
96
|
+
return self._cargo(crate_dir, ["check", "--quiet"], "build")
|
|
97
|
+
|
|
98
|
+
def test(self, crate_dir):
|
|
99
|
+
return self._cargo(crate_dir, ["test", "--quiet"], "test")
|
|
100
|
+
|
|
101
|
+
def clippy(self, crate_dir):
|
|
102
|
+
return self._cargo(crate_dir, ["clippy", "--quiet"], "clippy")
|
|
103
|
+
|
|
104
|
+
def doc(self, crate_dir):
|
|
105
|
+
return self._cargo(crate_dir, ["doc", "--no-deps"], "doc")
|
|
106
|
+
|
|
107
|
+
def fmt(self, crate_dir):
|
|
108
|
+
# --check only: fmt reports drift, it never reformats (RUST LAW 3).
|
|
109
|
+
return self._cargo(crate_dir, ["fmt", "--check"], "fmt",
|
|
110
|
+
show_stdout=True)
|
|
111
|
+
|
|
112
|
+
def clean(self, crate_dir):
|
|
113
|
+
# The one delete (RUST LAW 4): cargo clean removes target/ only.
|
|
114
|
+
return self._cargo(crate_dir, ["clean"], "clean")
|
|
115
|
+
|
|
116
|
+
def run(self, crate_dir):
|
|
117
|
+
d = Path(crate_dir).expanduser()
|
|
118
|
+
if not (d / "Cargo.toml").is_file():
|
|
119
|
+
print(f"[FAIL] no Cargo.toml in {d}")
|
|
120
|
+
return 1
|
|
121
|
+
try:
|
|
122
|
+
# No capture: the program's own output belongs on the terminal.
|
|
123
|
+
r = subprocess.run(["cargo", "run", "--quiet"], cwd=str(d),
|
|
124
|
+
timeout=600)
|
|
125
|
+
except (FileNotFoundError, OSError):
|
|
126
|
+
print("[FAIL] cargo not found — install Rust to use rust_wizard run")
|
|
127
|
+
return 2
|
|
128
|
+
return r.returncode
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Entry point for tmuxwiz dispatch."""
|
|
2
|
+
import sys
|
|
3
|
+
from .lib import cli as internal_cli
|
|
4
|
+
|
|
5
|
+
def main(argv=None):
|
|
6
|
+
old_argv = sys.argv
|
|
7
|
+
if argv is not None:
|
|
8
|
+
sys.argv = [sys.argv[0]] + list(argv)
|
|
9
|
+
try:
|
|
10
|
+
return internal_cli.main()
|
|
11
|
+
finally:
|
|
12
|
+
sys.argv = old_argv
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__":
|
|
15
|
+
sys.exit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# wizard package init
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
"""tmux_wizard - sovereign tmux session manager (stdlib only)."""
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
SUBCOMMANDS = ("new", "attach", "kill", "list", "census", "rename",
|
|
6
|
+
"capture", "detach", "law", "version")
|
|
7
|
+
|
|
8
|
+
LAW_TEXT = """TMUX LAW (Tmux Wizard, the session keeper)
|
|
9
|
+
1. The Tmux Wizard manages sessions; the truth about them comes
|
|
10
|
+
from tmux itself, never from memory or invention.
|
|
11
|
+
2. tmux is always called with an argument list, never through
|
|
12
|
+
a shell.
|
|
13
|
+
3. capture reads a pane; it changes nothing.
|
|
14
|
+
4. detach frees the client; the session and its work keep
|
|
15
|
+
running.
|
|
16
|
+
5. kill is the only destroyer, and it names its session.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def package_version():
|
|
21
|
+
try:
|
|
22
|
+
from importlib import metadata
|
|
23
|
+
for _n in ("wizard-central-toolkit", "wiz-central-toolkit"):
|
|
24
|
+
try:
|
|
25
|
+
return metadata.version(_n)
|
|
26
|
+
except metadata.PackageNotFoundError:
|
|
27
|
+
continue
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
30
|
+
try:
|
|
31
|
+
from wiz_central_toolkit import __version__
|
|
32
|
+
return __version__
|
|
33
|
+
except Exception:
|
|
34
|
+
return "unknown"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _run(args):
|
|
38
|
+
"""Run tmux with an arg list. Never shell=True (injection law)."""
|
|
39
|
+
try:
|
|
40
|
+
proc = subprocess.run(
|
|
41
|
+
["tmux"] + args, capture_output=True, text=True, timeout=10
|
|
42
|
+
)
|
|
43
|
+
return proc.returncode, proc.stdout.strip(), proc.stderr.strip()
|
|
44
|
+
except FileNotFoundError:
|
|
45
|
+
return 127, "", "tmux not installed"
|
|
46
|
+
except subprocess.TimeoutExpired:
|
|
47
|
+
return 124, "", "tmux timed out"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _sessions():
|
|
51
|
+
"""Live session names. Ground truth from tmux itself, zero fabrication."""
|
|
52
|
+
rc, out, _ = _run(["ls", "-F", "#{session_name}"])
|
|
53
|
+
if rc != 0:
|
|
54
|
+
return []
|
|
55
|
+
return [line for line in out.splitlines() if line]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def cmd_new(name, start_dir):
|
|
59
|
+
if not name or "/" in name or ":" in name:
|
|
60
|
+
print("[FAIL] invalid session name")
|
|
61
|
+
return 1
|
|
62
|
+
if name in _sessions():
|
|
63
|
+
print("[FAIL] session '%s' already exists" % name)
|
|
64
|
+
return 1
|
|
65
|
+
rc, _, err = _run(["new-session", "-d", "-s", name, "-c", start_dir])
|
|
66
|
+
if rc == 0:
|
|
67
|
+
print("[OK] session '%s' created" % name)
|
|
68
|
+
else:
|
|
69
|
+
print("[FAIL] %s" % err)
|
|
70
|
+
return rc
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def cmd_attach(name):
|
|
74
|
+
if name not in _sessions():
|
|
75
|
+
print("[FAIL] no session '%s'" % name)
|
|
76
|
+
return 1
|
|
77
|
+
rc, _, err = _run(["attach-session", "-t", name])
|
|
78
|
+
if rc == 0:
|
|
79
|
+
print("[OK] attached to '%s'" % name)
|
|
80
|
+
else:
|
|
81
|
+
print("[FAIL] attach to '%s' failed%s" % (name, (": " + err) if err else ""))
|
|
82
|
+
return rc
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def cmd_kill(name):
|
|
86
|
+
if name not in _sessions():
|
|
87
|
+
print("[FAIL] no session '%s'" % name)
|
|
88
|
+
return 1
|
|
89
|
+
rc, _, _ = _run(["kill-session", "-t", name])
|
|
90
|
+
print("[OK] session '%s' killed" % name if rc == 0 else "[FAIL] kill failed")
|
|
91
|
+
return rc
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def cmd_list():
|
|
95
|
+
sessions = _sessions()
|
|
96
|
+
if not sessions:
|
|
97
|
+
print("no sessions running")
|
|
98
|
+
return 0
|
|
99
|
+
for s in sessions:
|
|
100
|
+
print(s)
|
|
101
|
+
return 0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def cmd_census():
|
|
105
|
+
"""Fleet-style census: every session, window count, attach state."""
|
|
106
|
+
sessions = _sessions()
|
|
107
|
+
total = len(sessions)
|
|
108
|
+
print("sessions total: %d" % total)
|
|
109
|
+
ok = 0
|
|
110
|
+
for s in sessions:
|
|
111
|
+
rc, out, _ = _run(
|
|
112
|
+
["list-windows", "-t", s, "-F", "#{window_index}:#{window_name}"]
|
|
113
|
+
)
|
|
114
|
+
windows = out.splitlines() if rc == 0 else []
|
|
115
|
+
rc2, out2, _ = _run(["display-message", "-p", "-t", s, "#{session_attached}"])
|
|
116
|
+
attached = "ATTACHED" if out2 == "1" else "detached"
|
|
117
|
+
print("%s %-20s windows=%2d %s" % ("[OK]" if rc == 0 else "[FAIL]", s, len(windows), attached))
|
|
118
|
+
if rc == 0:
|
|
119
|
+
ok += 1
|
|
120
|
+
print("census verdict: %d/%d healthy" % (ok, total))
|
|
121
|
+
return 0 if ok == total else 1
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def cmd_capture(name, lines=200):
|
|
125
|
+
if name not in _sessions():
|
|
126
|
+
print("[FAIL] no session '%s'" % name)
|
|
127
|
+
return 1
|
|
128
|
+
rc, out, err = _run(["capture-pane", "-t", name, "-p", "-S", "-%d" % lines])
|
|
129
|
+
if rc == 0:
|
|
130
|
+
print(out)
|
|
131
|
+
else:
|
|
132
|
+
print("[FAIL] capture from '%s' failed%s" % (name, (": " + err) if err else ""))
|
|
133
|
+
return rc
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def cmd_detach(name):
|
|
137
|
+
if name not in _sessions():
|
|
138
|
+
print("[FAIL] no session '%s'" % name)
|
|
139
|
+
return 1
|
|
140
|
+
rc, _, err = _run(["detach-client", "-s", name])
|
|
141
|
+
if rc == 0:
|
|
142
|
+
print("[OK] clients detached from '%s' (session keeps running)" % name)
|
|
143
|
+
else:
|
|
144
|
+
print("[FAIL] detach from '%s' failed%s" % (name, (": " + err) if err else ""))
|
|
145
|
+
return rc
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def cmd_rename(old, new):
|
|
149
|
+
if old not in _sessions():
|
|
150
|
+
print("[FAIL] no session '%s'" % old)
|
|
151
|
+
return 1
|
|
152
|
+
if not new or "/" in new or ":" in new:
|
|
153
|
+
print("[FAIL] invalid new name")
|
|
154
|
+
return 1
|
|
155
|
+
rc, _, err = _run(["rename-session", "-t", old, new])
|
|
156
|
+
print("[OK] '%s' -> '%s'" % (old, new) if rc == 0 else "[FAIL] %s" % err)
|
|
157
|
+
return rc
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def main(argv):
|
|
161
|
+
if not argv or argv[0] in ("-h", "--help", "help"):
|
|
162
|
+
print("Tmux Wizard — sovereign tmux session manager")
|
|
163
|
+
print("alias: tmuxwiz")
|
|
164
|
+
print("usage: tmux_wizard <new|attach|kill|list|census|rename|capture|detach|law|version> [args]")
|
|
165
|
+
print(" new <name> [dir] create session (default cwd)")
|
|
166
|
+
print(" attach <name> join session")
|
|
167
|
+
print(" kill <name> destroy session")
|
|
168
|
+
print(" list names of live sessions")
|
|
169
|
+
print(" census health of every session")
|
|
170
|
+
print(" rename <old> <new> rename session")
|
|
171
|
+
print(" capture <name> [lines] print a pane's recent output (read-only)")
|
|
172
|
+
print(" detach <name> detach clients; the session keeps running")
|
|
173
|
+
print(" law print the TMUX LAW alone")
|
|
174
|
+
print(" version print the version")
|
|
175
|
+
return 0
|
|
176
|
+
cmd, rest = argv[0], argv[1:]
|
|
177
|
+
if cmd == "law":
|
|
178
|
+
print(LAW_TEXT)
|
|
179
|
+
return 0
|
|
180
|
+
if cmd == "version":
|
|
181
|
+
print("tmux_wizard %s" % package_version())
|
|
182
|
+
return 0
|
|
183
|
+
if cmd not in SUBCOMMANDS:
|
|
184
|
+
print("out of scope - unknown command: %s" % cmd)
|
|
185
|
+
return 2
|
|
186
|
+
if cmd == "new":
|
|
187
|
+
if not rest:
|
|
188
|
+
print("[FAIL] new requires a session name")
|
|
189
|
+
return 2
|
|
190
|
+
return cmd_new(rest[0], rest[1] if len(rest) > 1 else ".")
|
|
191
|
+
if cmd == "attach":
|
|
192
|
+
if not rest:
|
|
193
|
+
print("[FAIL] attach requires a name")
|
|
194
|
+
return 2
|
|
195
|
+
return cmd_attach(rest[0])
|
|
196
|
+
if cmd == "kill":
|
|
197
|
+
if not rest:
|
|
198
|
+
print("[FAIL] kill requires a name")
|
|
199
|
+
return 2
|
|
200
|
+
return cmd_kill(rest[0])
|
|
201
|
+
if cmd == "rename":
|
|
202
|
+
if len(rest) < 2:
|
|
203
|
+
print("[FAIL] rename requires old and new names")
|
|
204
|
+
return 2
|
|
205
|
+
return cmd_rename(rest[0], rest[1])
|
|
206
|
+
if cmd == "capture":
|
|
207
|
+
if not rest:
|
|
208
|
+
print("[FAIL] capture requires a session name")
|
|
209
|
+
return 2
|
|
210
|
+
lines = 200
|
|
211
|
+
if len(rest) > 1:
|
|
212
|
+
try:
|
|
213
|
+
lines = int(rest[1])
|
|
214
|
+
except ValueError:
|
|
215
|
+
print("[FAIL] capture lines must be a number")
|
|
216
|
+
return 2
|
|
217
|
+
return cmd_capture(rest[0], lines)
|
|
218
|
+
if cmd == "detach":
|
|
219
|
+
if not rest:
|
|
220
|
+
print("[FAIL] detach requires a name")
|
|
221
|
+
return 2
|
|
222
|
+
return cmd_detach(rest[0])
|
|
223
|
+
if cmd == "list":
|
|
224
|
+
return cmd_list()
|
|
225
|
+
return cmd_census()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
if __name__ == "__main__":
|
|
229
|
+
raise SystemExit(main(__import__("sys").argv[1:]))
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Entry point for wiz dispatch."""
|
|
2
|
+
import sys
|
|
3
|
+
from .lib import cli as internal_cli
|
|
4
|
+
from .lib.todo_wizard import TodoWizard # noqa: F401 (re-exported for tests)
|
|
5
|
+
|
|
6
|
+
# Wrap main() to accept argv (wiz dispatcher calls main(args))
|
|
7
|
+
def main(argv=None):
|
|
8
|
+
# Save original sys.argv
|
|
9
|
+
old_argv = sys.argv
|
|
10
|
+
# Replace with provided argv (or empty list)
|
|
11
|
+
if argv is not None:
|
|
12
|
+
sys.argv = [sys.argv[0]] + list(argv)
|
|
13
|
+
try:
|
|
14
|
+
return internal_cli.main()
|
|
15
|
+
finally:
|
|
16
|
+
sys.argv = old_argv
|
|
17
|
+
|
|
18
|
+
# Also expose for module-level access
|
|
19
|
+
__all__ = ['main', 'TodoWizard']
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
sys.exit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# wizard package init
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""todo-wizard CLI front-end."""
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
4
|
+
from .todo_wizard import TodoWizard
|
|
5
|
+
|
|
6
|
+
def main() -> int:
|
|
7
|
+
args = sys.argv[1:]
|
|
8
|
+
if not args or args[0] in ("-h", "--help", "help"):
|
|
9
|
+
print("Todo Wizard — standalone todo wizard")
|
|
10
|
+
print("alias: todowiz")
|
|
11
|
+
print("usage: todo_wizard <command> [args]")
|
|
12
|
+
print(" add <text> add a task (default priority normal)")
|
|
13
|
+
print(" list list all tasks")
|
|
14
|
+
print(" done <id> complete a task")
|
|
15
|
+
print(" undo <id> reopen a completed task")
|
|
16
|
+
print(" note <id> <text> append a progress note to a task")
|
|
17
|
+
print(" oldest show the stalest pending task")
|
|
18
|
+
print(" overdue [days] pending tasks older than N days (default 7)")
|
|
19
|
+
print(" archive-done move completed tasks to archive")
|
|
20
|
+
print(" rm <id> remove a task")
|
|
21
|
+
print(" stats JSON summary")
|
|
22
|
+
print(" export full JSON dump")
|
|
23
|
+
return 0
|
|
24
|
+
|
|
25
|
+
wiz = TodoWizard()
|
|
26
|
+
cmd = args[0]
|
|
27
|
+
|
|
28
|
+
if cmd == "add":
|
|
29
|
+
text = " ".join(args[1:]) or "untitled"
|
|
30
|
+
item = wiz.add(text)
|
|
31
|
+
print(f"[+] #{item['id']} {item['text']}")
|
|
32
|
+
elif cmd == "list":
|
|
33
|
+
for t in wiz.list_todos():
|
|
34
|
+
mark = "x" if t["done"] else " "
|
|
35
|
+
n_notes = len(t.get("notes", []))
|
|
36
|
+
suffix = f" [{n_notes} notes]" if n_notes else ""
|
|
37
|
+
print(f"[{mark}] #{t['id']} ({t['priority']}) {t['text']}{suffix}")
|
|
38
|
+
elif cmd == "done":
|
|
39
|
+
ok = wiz.complete(int(args[1]))
|
|
40
|
+
print("[OK] completed" if ok else "[FAIL] not found or already done")
|
|
41
|
+
return 0 if ok else 1
|
|
42
|
+
elif cmd == "undo":
|
|
43
|
+
ok = wiz.undo_complete(int(args[1]))
|
|
44
|
+
print("[OK] reopened" if ok else "[FAIL] not found or not done")
|
|
45
|
+
return 0 if ok else 1
|
|
46
|
+
elif cmd == "note":
|
|
47
|
+
if len(args) < 3:
|
|
48
|
+
print("out of scope - note requires id and text")
|
|
49
|
+
return 2
|
|
50
|
+
ok = wiz.note(int(args[1]), " ".join(args[2:]))
|
|
51
|
+
print("[OK] noted" if ok else "[FAIL] no such task")
|
|
52
|
+
return 0 if ok else 1
|
|
53
|
+
elif cmd == "oldest":
|
|
54
|
+
t = wiz.oldest()
|
|
55
|
+
if t is None:
|
|
56
|
+
print("no pending tasks")
|
|
57
|
+
else:
|
|
58
|
+
print(f"#{t['id']} {t['text']} (created {t['created']})")
|
|
59
|
+
elif cmd == "overdue":
|
|
60
|
+
days = int(args[1]) if len(args) > 1 else 7
|
|
61
|
+
results = wiz.overdue(days)
|
|
62
|
+
if not results:
|
|
63
|
+
print(f"no pending tasks older than {days} days")
|
|
64
|
+
else:
|
|
65
|
+
for t, age in results:
|
|
66
|
+
print(f"#{t['id']} {t['text']} — {age:.1f} days old (created {t['created']})")
|
|
67
|
+
return 1 if results else 0
|
|
68
|
+
elif cmd == "archive-done":
|
|
69
|
+
moved = wiz.archive_done()
|
|
70
|
+
print("[OK] archived %d completed tasks" % moved if moved else "no completed tasks to archive")
|
|
71
|
+
elif cmd == "rm":
|
|
72
|
+
ok = wiz.remove(int(args[1]))
|
|
73
|
+
print("[OK] removed" if ok else "[FAIL] not found")
|
|
74
|
+
return 0 if ok else 1
|
|
75
|
+
elif cmd == "stats":
|
|
76
|
+
print(json.dumps(wiz.stats()))
|
|
77
|
+
elif cmd == "export":
|
|
78
|
+
print(json.dumps(wiz.list_todos(), indent=2))
|
|
79
|
+
else:
|
|
80
|
+
print("out of scope")
|
|
81
|
+
return 2
|
|
82
|
+
return 0
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""todo-wizard core: standalone, importable todo engine."""
|
|
2
|
+
import json
|
|
3
|
+
import hashlib
|
|
4
|
+
import datetime as dt
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
class TodoWizard:
|
|
9
|
+
"""Per-project persistent todo engine. Storage lives in ~/.todo-wizard/."""
|
|
10
|
+
|
|
11
|
+
BANK = Path.home() / ".todo-wizard"
|
|
12
|
+
|
|
13
|
+
def __init__(self, project_dir: Optional[str] = None, bank: Optional[Path] = None):
|
|
14
|
+
self.project = Path(project_dir or Path.cwd()).resolve()
|
|
15
|
+
self.bank = bank or self.BANK
|
|
16
|
+
self.store_path = self._store_for(self.project)
|
|
17
|
+
|
|
18
|
+
def _store_for(self, path: Path) -> Path:
|
|
19
|
+
key = hashlib.sha256(str(path).encode()).hexdigest()[:16]
|
|
20
|
+
return self.bank / f"{path.name}-{key}.json"
|
|
21
|
+
|
|
22
|
+
def _load(self) -> dict:
|
|
23
|
+
if self.store_path.exists():
|
|
24
|
+
return json.loads(self.store_path.read_text())
|
|
25
|
+
return {"project": str(self.project), "todos": [], "created": self._now()}
|
|
26
|
+
|
|
27
|
+
def _save(self, data: dict) -> None:
|
|
28
|
+
self.bank.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
tmp = self.store_path.with_suffix(".tmp")
|
|
30
|
+
tmp.write_text(json.dumps(data, indent=2))
|
|
31
|
+
tmp.replace(self.store_path)
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def _now() -> str:
|
|
35
|
+
return dt.datetime.now().isoformat(timespec="seconds")
|
|
36
|
+
|
|
37
|
+
def add(self, text: str, priority: str = "normal") -> dict:
|
|
38
|
+
data = self._load()
|
|
39
|
+
item = {
|
|
40
|
+
"id": max((t["id"] for t in data["todos"] + data.get("archive", [])), default=0) + 1,
|
|
41
|
+
"text": text,
|
|
42
|
+
"priority": priority,
|
|
43
|
+
"done": False,
|
|
44
|
+
"created": self._now(),
|
|
45
|
+
"completed": None,
|
|
46
|
+
}
|
|
47
|
+
data["todos"].append(item)
|
|
48
|
+
data["last_modified"] = self._now()
|
|
49
|
+
self._save(data)
|
|
50
|
+
return item
|
|
51
|
+
|
|
52
|
+
def complete(self, todo_id: int) -> bool:
|
|
53
|
+
data = self._load()
|
|
54
|
+
for item in data["todos"]:
|
|
55
|
+
if item["id"] == todo_id and not item["done"]:
|
|
56
|
+
item["done"] = True
|
|
57
|
+
item["completed"] = self._now()
|
|
58
|
+
self._save(data)
|
|
59
|
+
return True
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
def remove(self, todo_id: int) -> bool:
|
|
63
|
+
data = self._load()
|
|
64
|
+
before = len(data["todos"])
|
|
65
|
+
data["todos"] = [t for t in data["todos"] if t["id"] != todo_id]
|
|
66
|
+
if len(data["todos"]) < before:
|
|
67
|
+
self._save(data)
|
|
68
|
+
return True
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
def list_todos(self, pending_only: bool = False) -> list:
|
|
72
|
+
todos = self._load()["todos"]
|
|
73
|
+
return [t for t in todos if not t["done"]] if pending_only else todos
|
|
74
|
+
|
|
75
|
+
def stats(self) -> dict:
|
|
76
|
+
todos = self.list_todos()
|
|
77
|
+
done = sum(1 for t in todos if t["done"])
|
|
78
|
+
return {"total": len(todos), "done": done, "pending": len(todos) - done}
|
|
79
|
+
|
|
80
|
+
def undo_complete(self, todo_id: int) -> bool:
|
|
81
|
+
"""Reopen a closed task."""
|
|
82
|
+
data = self._load()
|
|
83
|
+
for item in data["todos"]:
|
|
84
|
+
if item["id"] == todo_id and item["done"]:
|
|
85
|
+
item["done"] = False
|
|
86
|
+
item["completed"] = None
|
|
87
|
+
data["last_modified"] = self._now()
|
|
88
|
+
self._save(data)
|
|
89
|
+
return True
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
def note(self, todo_id: int, text: str) -> bool:
|
|
93
|
+
"""Append a progress note to a task (evidence trail)."""
|
|
94
|
+
data = self._load()
|
|
95
|
+
for item in data["todos"]:
|
|
96
|
+
if item["id"] == todo_id:
|
|
97
|
+
notes = item.setdefault("notes", [])
|
|
98
|
+
notes.append({"text": text, "at": self._now()})
|
|
99
|
+
data["last_modified"] = self._now()
|
|
100
|
+
self._save(data)
|
|
101
|
+
return True
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
def oldest(self) -> Optional[dict]:
|
|
105
|
+
"""Return the stalest pending task."""
|
|
106
|
+
pending = self.list_todos(pending_only=True)
|
|
107
|
+
if not pending:
|
|
108
|
+
return None
|
|
109
|
+
return min(pending, key=lambda t: t["created"])
|
|
110
|
+
|
|
111
|
+
def archive_done(self) -> int:
|
|
112
|
+
"""Move completed tasks to the archive list (never destroy)."""
|
|
113
|
+
data = self._load()
|
|
114
|
+
done_items = [t for t in data["todos"] if t["done"]]
|
|
115
|
+
if not done_items:
|
|
116
|
+
return 0
|
|
117
|
+
archive = data.setdefault("archive", [])
|
|
118
|
+
archive.extend(done_items)
|
|
119
|
+
archive.sort(key=lambda t: t.get("completed") or "")
|
|
120
|
+
data["todos"] = [t for t in data["todos"] if not t["done"]]
|
|
121
|
+
data["last_modified"] = self._now()
|
|
122
|
+
self._save(data)
|
|
123
|
+
return len(done_items)
|
|
124
|
+
|
|
125
|
+
def overdue(self, days: int = 7) -> list:
|
|
126
|
+
"""Pending tasks older than N days."""
|
|
127
|
+
cutoff = dt.datetime.now() - dt.timedelta(days=days)
|
|
128
|
+
result = []
|
|
129
|
+
for t in self.list_todos(pending_only=True):
|
|
130
|
+
created = dt.datetime.fromisoformat(t["created"])
|
|
131
|
+
age_days = (dt.datetime.now() - created).total_seconds() / 86400
|
|
132
|
+
if created < cutoff:
|
|
133
|
+
result.append((t, age_days))
|
|
134
|
+
return result
|