feat(systemd): add and enable continuous tmux-auto-approver user daemon
This commit is contained in:
+64
-4
@@ -953,8 +953,8 @@ def cmd_runtime(args):
|
||||
print(c_dim(" No panes found."))
|
||||
print()
|
||||
return
|
||||
headers = ["SOCKET", "SESSION", "PANE", "CMD", "STATE",
|
||||
"APPROVE", "WATCHER"]
|
||||
headers = ["SOCKET", "SESSION", "NODE", "PANE", "CMD",
|
||||
"STATE", "APPROVE", "WATCHER"]
|
||||
table = []
|
||||
for r in rows:
|
||||
state = r["state"]
|
||||
@@ -970,6 +970,7 @@ def cmd_runtime(args):
|
||||
if r["watcher_alive"] else badge_dim("-"))
|
||||
table.append([os.path.basename(r["socket"]),
|
||||
"%s:%s" % (r["session"], r["window"]),
|
||||
r["node"] or badge_dim("-"),
|
||||
r["pane"], (r["cmd"] or "")[:26], state,
|
||||
approve, watcher])
|
||||
print_table(headers, table)
|
||||
@@ -1149,8 +1150,12 @@ def cmd_runtime(args):
|
||||
spread, failed = [], []
|
||||
for t in targets:
|
||||
name = "spread-%s" % t["pane"]
|
||||
# -t session: pins the new window to the SOURCE session.
|
||||
# Without it break-pane follows the ATTACHED session and
|
||||
# flings panes across sessions (observed live on scratch).
|
||||
r = mcw._tmux(t["socket"], "break-pane", "-s", t["pane"],
|
||||
"-n", name, timeout=15)
|
||||
"-t", "%s:" % t["session"], "-n", name,
|
||||
timeout=15)
|
||||
if r.returncode == 0:
|
||||
spread.append("%s:%s" % (t["session"], t["pane"]))
|
||||
else:
|
||||
@@ -1272,13 +1277,51 @@ def cmd_muse_choices(args):
|
||||
print(line.rstrip("\n"))
|
||||
print()
|
||||
|
||||
elif action == "resolve":
|
||||
sock = getattr(args, "socket", None)
|
||||
pane = getattr(args, "pane", None)
|
||||
decision = getattr(args, "decision", None)
|
||||
hf = mcw.read_hold(sock, pane)
|
||||
if not hf:
|
||||
if as_json:
|
||||
print(json.dumps({"ok": True, "held": None,
|
||||
"note": "nothing held"}, indent=2))
|
||||
return
|
||||
print(c_dim("\n Nothing held on %s:%s.\n" % (sock, pane)))
|
||||
return
|
||||
if decision == "deny" and hf.get("kind") not in mcw.NEGATIVE_KEYS:
|
||||
msg = ("refusing deny on %s prompt (D2: questions always "
|
||||
"resolve top-choice); use approve or wait for expiry"
|
||||
% (hf.get("kind") or "?"))
|
||||
if as_json:
|
||||
print(json.dumps({"ok": False, "reason": msg}, indent=2))
|
||||
return
|
||||
print(c_red("\n Error: " + msg + "\n"), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
hf["directive"] = decision
|
||||
mcw.write_hold(sock, pane, hf)
|
||||
mcw.audit("muse-choice-resolved",
|
||||
name="%s:%s" % (os.path.basename(sock), pane),
|
||||
caller=caller,
|
||||
extra={"socket": sock, "pane": pane,
|
||||
"decision": decision, "sig": hf.get("sig")})
|
||||
if as_json:
|
||||
print(json.dumps({"ok": True, "resolved": decision,
|
||||
"pane": pane}, indent=2))
|
||||
return
|
||||
print(c_green("\n✔ Hold on %s:%s will %s within a poll."
|
||||
% (sock, pane,
|
||||
"approve" if decision == "approve" else "deny")))
|
||||
print()
|
||||
|
||||
else: # status
|
||||
desired = mcw.get_desired()
|
||||
watchers = mcw.status_all()
|
||||
answers = mcw.recent_answers(5)
|
||||
held = mcw.list_holds()
|
||||
if as_json:
|
||||
print(json.dumps({"ok": True, "desired": desired,
|
||||
"watchers": watchers,
|
||||
"watchers": watchers, "held": held,
|
||||
"recent_answers": answers}, indent=2))
|
||||
return
|
||||
state_badge = badge_ok("ON") if desired["enabled"] else badge_dim("OFF")
|
||||
@@ -1302,6 +1345,18 @@ def cmd_muse_choices(args):
|
||||
print_table(headers, rows)
|
||||
else:
|
||||
print(c_dim(" No watcher state files."))
|
||||
if held:
|
||||
print(c_bold("\n Held prompts (resolve via: box muse-choices resolve --socket S --pane P approve|deny):"))
|
||||
for h in held:
|
||||
try:
|
||||
left = max(0, int(float(h.get("held_until", 0)) - time.time()))
|
||||
except (TypeError, ValueError):
|
||||
left = -1
|
||||
print(f" • {badge_warn('HELD')} {c_bold(h.get('pane', '?'))} "
|
||||
f"{h.get('kind')}/{h.get('key')} rule={h.get('rule')} "
|
||||
f"expires in {left}s")
|
||||
print(f" {c_dim((h.get('reason') or '')[:100])}")
|
||||
print(f" {c_dim((h.get('text') or '')[:100])}")
|
||||
if answers:
|
||||
print(c_bold("\n Recent answers (box audit feed):"))
|
||||
for a in answers:
|
||||
@@ -5634,6 +5689,11 @@ def build_parser():
|
||||
|
||||
p_mc_rec = mc_sub.add_parser("reconcile", parents=[common], help="Enforce desired state now (start missing / stop excess)")
|
||||
|
||||
p_mc_res = mc_sub.add_parser("resolve", parents=[common], help="Resolve a held prompt (approve now or deny it)")
|
||||
p_mc_res.add_argument("--socket", required=True, help="Tmux socket path (e.g. /tmp/tmux-1000/default)")
|
||||
p_mc_res.add_argument("--pane", required=True, help="Pane id (e.g. %%37)")
|
||||
p_mc_res.add_argument("decision", choices=["approve", "deny"], help="Release the hold to approve, or deny it (permission kinds only)")
|
||||
|
||||
# Domain: RUNTIME
|
||||
p_rt = subparsers.add_parser("runtime", parents=[common], help="Muse CLI tmux runtimes: list states, send input, launch auto-approved")
|
||||
rt_sub = p_rt.add_subparsers(dest="rt_action")
|
||||
|
||||
Reference in New Issue
Block a user