missioncache-dashboard 1.0.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.
@@ -0,0 +1,3 @@
1
+ """MissionCache Dashboard - Task analytics and autonomous execution monitoring."""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,349 @@
1
+ """MissionCache Dashboard CLI.
2
+
3
+ Entry point for the `missioncache-dashboard` console script. Subcommands:
4
+
5
+ missioncache-dashboard serve Run the dashboard (default).
6
+ missioncache-dashboard install-service Register as launchd / systemd service.
7
+ missioncache-dashboard uninstall-service Remove the service.
8
+ missioncache-dashboard reinstall-service Uninstall + install (Python path fix).
9
+ missioncache-dashboard status Show installed / running state.
10
+
11
+ Platform support: macOS (launchd) and Linux (systemd --user). Windows
12
+ prints manual instructions and exits 0 - Task Scheduler support is
13
+ deferred.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import os
20
+ import shutil
21
+ import socket
22
+ import subprocess
23
+ import sys
24
+ from pathlib import Path
25
+
26
+ LAUNCHD_LABEL = "com.missioncache.dashboard"
27
+ SYSTEMD_UNIT = "missioncache-dashboard.service"
28
+ DEFAULT_PORT = 8787
29
+
30
+
31
+ # =============================================================================
32
+ # Paths
33
+ # =============================================================================
34
+
35
+
36
+ def launchd_plist_path() -> Path:
37
+ return Path.home() / "Library" / "LaunchAgents" / f"{LAUNCHD_LABEL}.plist"
38
+
39
+
40
+ def systemd_unit_path() -> Path:
41
+ return Path.home() / ".config" / "systemd" / "user" / SYSTEMD_UNIT
42
+
43
+
44
+ def log_dir() -> Path:
45
+ return Path.home() / ".claude" / "logs"
46
+
47
+
48
+ # =============================================================================
49
+ # Templates (pure, testable)
50
+ # =============================================================================
51
+
52
+
53
+ def render_plist(binary_path: str, port: int) -> str:
54
+ """Render the launchd plist pointing at the pip-installed binary."""
55
+ logs = log_dir()
56
+ env_block = ""
57
+ if port != DEFAULT_PORT:
58
+ env_block = (
59
+ " <key>EnvironmentVariables</key>\n"
60
+ " <dict>\n"
61
+ f" <key>MISSIONCACHE_DASHBOARD_PORT</key>\n"
62
+ f" <string>{port}</string>\n"
63
+ " </dict>\n"
64
+ )
65
+ return (
66
+ '<?xml version="1.0" encoding="UTF-8"?>\n'
67
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n'
68
+ '<plist version="1.0">\n'
69
+ "<dict>\n"
70
+ " <key>Label</key>\n"
71
+ f" <string>{LAUNCHD_LABEL}</string>\n"
72
+ " <key>ProgramArguments</key>\n"
73
+ " <array>\n"
74
+ f" <string>{binary_path}</string>\n"
75
+ " <string>serve</string>\n"
76
+ " </array>\n"
77
+ " <key>RunAtLoad</key>\n"
78
+ " <true/>\n"
79
+ " <key>KeepAlive</key>\n"
80
+ " <true/>\n"
81
+ " <key>StandardOutPath</key>\n"
82
+ f" <string>{logs / 'missioncache-dashboard-stdout.log'}</string>\n"
83
+ " <key>StandardErrorPath</key>\n"
84
+ f" <string>{logs / 'missioncache-dashboard-stderr.log'}</string>\n"
85
+ f"{env_block}"
86
+ "</dict>\n"
87
+ "</plist>\n"
88
+ )
89
+
90
+
91
+ def render_systemd_unit(binary_path: str, port: int) -> str:
92
+ """Render the systemd user unit pointing at the pip-installed binary."""
93
+ env_line = f"Environment=MISSIONCACHE_DASHBOARD_PORT={port}\n" if port != DEFAULT_PORT else ""
94
+ return (
95
+ "[Unit]\n"
96
+ "Description=MissionCache Dashboard\n"
97
+ "After=network.target\n"
98
+ "\n"
99
+ "[Service]\n"
100
+ "Type=simple\n"
101
+ f"{env_line}"
102
+ f"ExecStart={binary_path} serve\n"
103
+ "Restart=always\n"
104
+ "RestartSec=5\n"
105
+ "\n"
106
+ "[Install]\n"
107
+ "WantedBy=default.target\n"
108
+ )
109
+
110
+
111
+ # =============================================================================
112
+ # Port probing
113
+ # =============================================================================
114
+
115
+
116
+ def port_in_use(port: int) -> bool:
117
+ """Return True if TCP port is bound on 127.0.0.1."""
118
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
119
+ sock.settimeout(0.5)
120
+ try:
121
+ sock.bind(("127.0.0.1", port))
122
+ except OSError:
123
+ return True
124
+ return False
125
+
126
+
127
+ def resolve_port(requested: int) -> int:
128
+ """Return a free port, prompting if the requested one is taken."""
129
+ if not port_in_use(requested):
130
+ return requested
131
+ print(f" Port {requested} is already in use.")
132
+ while True:
133
+ raw = input(" Enter a different port (or blank to abort): ").strip()
134
+ if not raw:
135
+ raise SystemExit("Aborted.")
136
+ try:
137
+ alt = int(raw)
138
+ except ValueError:
139
+ print(" Not a number, try again.")
140
+ continue
141
+ if port_in_use(alt):
142
+ print(f" Port {alt} is also in use.")
143
+ continue
144
+ return alt
145
+
146
+
147
+ # =============================================================================
148
+ # Binary resolution
149
+ # =============================================================================
150
+
151
+
152
+ def resolve_binary() -> str:
153
+ """Return the absolute path of the installed `missioncache-dashboard` script."""
154
+ found = shutil.which("missioncache-dashboard")
155
+ if not found:
156
+ raise SystemExit(
157
+ "Could not find `missioncache-dashboard` on PATH. This command must be "
158
+ "run from the same environment where `missioncache-dashboard` is pip-"
159
+ "installed (pipx, uv tool, or a venv)."
160
+ )
161
+ return found
162
+
163
+
164
+ # =============================================================================
165
+ # Platform install/uninstall
166
+ # =============================================================================
167
+
168
+
169
+ def install_launchd(port: int) -> None:
170
+ binary = resolve_binary()
171
+ plist = launchd_plist_path()
172
+ plist.parent.mkdir(parents=True, exist_ok=True)
173
+ log_dir().mkdir(parents=True, exist_ok=True)
174
+
175
+ if plist.exists():
176
+ print(f" Replacing existing service definition at {plist}")
177
+ subprocess.run(["launchctl", "unload", str(plist)], check=False)
178
+
179
+ plist.write_text(render_plist(binary, port))
180
+ subprocess.run(["launchctl", "load", str(plist)], check=True)
181
+ print(f" launchd service loaded: {LAUNCHD_LABEL}")
182
+ print(f" Logs: {log_dir()}/missioncache-dashboard-{{stdout,stderr}}.log")
183
+
184
+
185
+ def uninstall_launchd() -> None:
186
+ plist = launchd_plist_path()
187
+ if not plist.exists():
188
+ print(" launchd service not installed, nothing to do.")
189
+ return
190
+ subprocess.run(["launchctl", "unload", str(plist)], check=False)
191
+ plist.unlink()
192
+ print(f" Removed {plist}")
193
+
194
+
195
+ def install_systemd(port: int) -> None:
196
+ binary = resolve_binary()
197
+ unit = systemd_unit_path()
198
+ unit.parent.mkdir(parents=True, exist_ok=True)
199
+ unit.write_text(render_systemd_unit(binary, port))
200
+ subprocess.run(["systemctl", "--user", "daemon-reload"], check=True)
201
+ subprocess.run(["systemctl", "--user", "enable", "--now", SYSTEMD_UNIT], check=True)
202
+ print(f" systemd --user unit enabled: {SYSTEMD_UNIT}")
203
+
204
+
205
+ def uninstall_systemd() -> None:
206
+ unit = systemd_unit_path()
207
+ if not unit.exists():
208
+ print(" systemd user unit not installed, nothing to do.")
209
+ return
210
+ subprocess.run(["systemctl", "--user", "disable", "--now", SYSTEMD_UNIT], check=False)
211
+ unit.unlink()
212
+ subprocess.run(["systemctl", "--user", "daemon-reload"], check=False)
213
+ print(f" Removed {unit}")
214
+
215
+
216
+ # =============================================================================
217
+ # Subcommand handlers
218
+ # =============================================================================
219
+
220
+
221
+ def cmd_serve(_args: argparse.Namespace) -> int:
222
+ """Run the dashboard via uvicorn. Reads MISSIONCACHE_DASHBOARD_PORT env var."""
223
+ import uvicorn # local import: keeps `missioncache-dashboard --help` fast
224
+
225
+ port = int(os.environ.get("MISSIONCACHE_DASHBOARD_PORT", str(DEFAULT_PORT)))
226
+ uvicorn.run("missioncache_dashboard.server:app", host="127.0.0.1", port=port)
227
+ return 0
228
+
229
+
230
+ def cmd_install_service(_args: argparse.Namespace) -> int:
231
+ port = int(os.environ.get("MISSIONCACHE_DASHBOARD_PORT", str(DEFAULT_PORT)))
232
+ port = resolve_port(port)
233
+
234
+ if sys.platform == "darwin":
235
+ install_launchd(port)
236
+ elif sys.platform.startswith("linux"):
237
+ install_systemd(port)
238
+ elif sys.platform == "win32":
239
+ print(
240
+ "Windows service registration is not yet supported.\n"
241
+ "Run 'missioncache-dashboard serve' manually, or add your own Task "
242
+ "Scheduler entry. See docs/installation.md#windows."
243
+ )
244
+ return 0
245
+ else:
246
+ print(f"Unsupported platform: {sys.platform}", file=sys.stderr)
247
+ return 1
248
+ return 0
249
+
250
+
251
+ def cmd_uninstall_service(_args: argparse.Namespace) -> int:
252
+ if sys.platform == "darwin":
253
+ uninstall_launchd()
254
+ elif sys.platform.startswith("linux"):
255
+ uninstall_systemd()
256
+ elif sys.platform == "win32":
257
+ print("Windows service was never auto-registered; nothing to uninstall.")
258
+ return 0
259
+ else:
260
+ print(f"Unsupported platform: {sys.platform}", file=sys.stderr)
261
+ return 1
262
+ return 0
263
+
264
+
265
+ def cmd_reinstall_service(args: argparse.Namespace) -> int:
266
+ rc = cmd_uninstall_service(args)
267
+ if rc != 0:
268
+ return rc
269
+ return cmd_install_service(args)
270
+
271
+
272
+ def cmd_status(_args: argparse.Namespace) -> int:
273
+ """Report installed and running state."""
274
+ if sys.platform == "darwin":
275
+ installed = launchd_plist_path().exists()
276
+ running = False
277
+ if installed:
278
+ result = subprocess.run(
279
+ ["launchctl", "list", LAUNCHD_LABEL],
280
+ capture_output=True,
281
+ text=True,
282
+ check=False,
283
+ )
284
+ running = result.returncode == 0
285
+ print(f" Installed: {installed}")
286
+ print(f" Running: {running}")
287
+ elif sys.platform.startswith("linux"):
288
+ installed = systemd_unit_path().exists()
289
+ running = False
290
+ if installed:
291
+ result = subprocess.run(
292
+ ["systemctl", "--user", "is-active", SYSTEMD_UNIT],
293
+ capture_output=True,
294
+ text=True,
295
+ check=False,
296
+ )
297
+ running = result.stdout.strip() == "active"
298
+ print(f" Installed: {installed}")
299
+ print(f" Running: {running}")
300
+ elif sys.platform == "win32":
301
+ print(" Windows: not supported.")
302
+ else:
303
+ print(f" Unsupported platform: {sys.platform}")
304
+ return 0
305
+
306
+
307
+ # =============================================================================
308
+ # argparse wiring
309
+ # =============================================================================
310
+
311
+
312
+ def build_parser() -> argparse.ArgumentParser:
313
+ parser = argparse.ArgumentParser(
314
+ prog="missioncache-dashboard",
315
+ description="MissionCache Dashboard - task analytics and autonomous execution monitoring.",
316
+ )
317
+ sub = parser.add_subparsers(dest="command")
318
+
319
+ p_serve = sub.add_parser("serve", help="Run the dashboard (default)")
320
+ p_serve.set_defaults(func=cmd_serve)
321
+
322
+ p_install = sub.add_parser("install-service", help="Register the dashboard as a background service")
323
+ p_install.set_defaults(func=cmd_install_service)
324
+
325
+ p_uninstall = sub.add_parser("uninstall-service", help="Remove the background service")
326
+ p_uninstall.set_defaults(func=cmd_uninstall_service)
327
+
328
+ p_reinstall = sub.add_parser(
329
+ "reinstall-service",
330
+ help="Uninstall + install the service (Python path change recovery)",
331
+ )
332
+ p_reinstall.set_defaults(func=cmd_reinstall_service)
333
+
334
+ p_status = sub.add_parser("status", help="Show service installed and running state")
335
+ p_status.set_defaults(func=cmd_status)
336
+
337
+ return parser
338
+
339
+
340
+ def main(argv: list[str] | None = None) -> int:
341
+ parser = build_parser()
342
+ args = parser.parse_args(argv)
343
+ if not getattr(args, "func", None):
344
+ return cmd_serve(args)
345
+ return args.func(args)
346
+
347
+
348
+ if __name__ == "__main__":
349
+ raise SystemExit(main())