code-context-control 2.28.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 (150) hide show
  1. cli/__init__.py +1 -0
  2. cli/_hook_utils.py +99 -0
  3. cli/c3.py +6152 -0
  4. cli/commands/__init__.py +1 -0
  5. cli/commands/common.py +312 -0
  6. cli/commands/parser.py +286 -0
  7. cli/docs.html +3178 -0
  8. cli/edits.html +878 -0
  9. cli/hook_auto_snapshot.py +142 -0
  10. cli/hook_c3_signal.py +61 -0
  11. cli/hook_c3read.py +116 -0
  12. cli/hook_edit_ledger.py +213 -0
  13. cli/hook_edit_unlock.py +170 -0
  14. cli/hook_filter.py +130 -0
  15. cli/hook_ghost_files.py +238 -0
  16. cli/hook_pretool_enforce.py +334 -0
  17. cli/hook_read.py +200 -0
  18. cli/hook_session_stats.py +62 -0
  19. cli/hook_terse_advisor.py +190 -0
  20. cli/hub.html +3764 -0
  21. cli/hub_server.py +1619 -0
  22. cli/mcp_proxy.py +428 -0
  23. cli/mcp_server.py +660 -0
  24. cli/server.py +2985 -0
  25. cli/tools/__init__.py +4 -0
  26. cli/tools/_helpers.py +65 -0
  27. cli/tools/agent.py +1165 -0
  28. cli/tools/compress.py +215 -0
  29. cli/tools/delegate.py +1184 -0
  30. cli/tools/edit.py +313 -0
  31. cli/tools/edits.py +118 -0
  32. cli/tools/filter.py +285 -0
  33. cli/tools/impact.py +163 -0
  34. cli/tools/memory.py +469 -0
  35. cli/tools/read.py +224 -0
  36. cli/tools/search.py +337 -0
  37. cli/tools/session.py +95 -0
  38. cli/tools/shell.py +193 -0
  39. cli/tools/status.py +306 -0
  40. cli/tools/validate.py +310 -0
  41. cli/ui/api.js +36 -0
  42. cli/ui/app.js +207 -0
  43. cli/ui/components/chat.js +758 -0
  44. cli/ui/components/dashboard.js +689 -0
  45. cli/ui/components/edits.js +220 -0
  46. cli/ui/components/instructions.js +481 -0
  47. cli/ui/components/memory.js +626 -0
  48. cli/ui/components/sessions.js +606 -0
  49. cli/ui/components/settings.js +1404 -0
  50. cli/ui/components/sidebar.js +156 -0
  51. cli/ui/icons.js +51 -0
  52. cli/ui/shared.js +119 -0
  53. cli/ui/theme.js +22 -0
  54. cli/ui.html +168 -0
  55. cli/ui_legacy.html +6797 -0
  56. cli/ui_nano.html +503 -0
  57. code_context_control-2.28.0.dist-info/METADATA +248 -0
  58. code_context_control-2.28.0.dist-info/RECORD +150 -0
  59. code_context_control-2.28.0.dist-info/WHEEL +5 -0
  60. code_context_control-2.28.0.dist-info/entry_points.txt +4 -0
  61. code_context_control-2.28.0.dist-info/licenses/LICENSE +201 -0
  62. code_context_control-2.28.0.dist-info/top_level.txt +5 -0
  63. core/__init__.py +75 -0
  64. core/config.py +269 -0
  65. core/ide.py +188 -0
  66. oracle/__init__.py +1 -0
  67. oracle/config.py +75 -0
  68. oracle/oracle.html +3900 -0
  69. oracle/oracle_server.py +663 -0
  70. oracle/services/__init__.py +1 -0
  71. oracle/services/c3_bridge.py +210 -0
  72. oracle/services/chat_engine.py +1103 -0
  73. oracle/services/chat_store.py +155 -0
  74. oracle/services/cross_memory.py +154 -0
  75. oracle/services/federated_graph.py +463 -0
  76. oracle/services/health_checker.py +117 -0
  77. oracle/services/insight_engine.py +307 -0
  78. oracle/services/memory_reader.py +106 -0
  79. oracle/services/memory_writer.py +182 -0
  80. oracle/services/ollama_bridge.py +332 -0
  81. oracle/services/project_scanner.py +87 -0
  82. oracle/services/review_agent.py +206 -0
  83. services/__init__.py +1 -0
  84. services/activity_log.py +93 -0
  85. services/agent_base.py +124 -0
  86. services/agents.py +1529 -0
  87. services/auto_memory.py +407 -0
  88. services/bench/__init__.py +6 -0
  89. services/bench/external/__init__.py +29 -0
  90. services/bench/external/aider_polyglot.py +405 -0
  91. services/bench/external/swe_bench.py +485 -0
  92. services/benchmark_dashboard.py +596 -0
  93. services/claude_md.py +785 -0
  94. services/compressor.py +592 -0
  95. services/context_snapshot.py +356 -0
  96. services/conversation_store.py +870 -0
  97. services/doc_index.py +537 -0
  98. services/e2e_benchmark.py +2884 -0
  99. services/e2e_evaluator.py +396 -0
  100. services/e2e_tasks.py +743 -0
  101. services/edit_ledger.py +459 -0
  102. services/embedding_index.py +341 -0
  103. services/error_reporting.py +123 -0
  104. services/file_memory.py +734 -0
  105. services/hub_service.py +585 -0
  106. services/indexer.py +712 -0
  107. services/memory.py +318 -0
  108. services/memory_consolidator.py +538 -0
  109. services/memory_graph.py +382 -0
  110. services/memory_grounder.py +304 -0
  111. services/memory_scorer.py +246 -0
  112. services/metrics.py +86 -0
  113. services/notifications.py +209 -0
  114. services/ollama_client.py +201 -0
  115. services/output_filter.py +488 -0
  116. services/parser.py +1238 -0
  117. services/project_manager.py +579 -0
  118. services/protocol.py +306 -0
  119. services/proxy_state.py +152 -0
  120. services/retrieval_broker.py +129 -0
  121. services/router.py +414 -0
  122. services/runtime.py +326 -0
  123. services/session_benchmark.py +1945 -0
  124. services/session_manager.py +1026 -0
  125. services/session_preloader.py +251 -0
  126. services/text_index.py +90 -0
  127. services/tool_classifier.py +176 -0
  128. services/transcript_index.py +340 -0
  129. services/validation_cache.py +155 -0
  130. services/vector_store.py +299 -0
  131. services/version_tracker.py +271 -0
  132. services/watcher.py +192 -0
  133. tui/__init__.py +0 -0
  134. tui/backend.py +59 -0
  135. tui/main.py +145 -0
  136. tui/screens/__init__.py +1 -0
  137. tui/screens/benchmark_view.py +109 -0
  138. tui/screens/claudemd_view.py +46 -0
  139. tui/screens/compress_view.py +52 -0
  140. tui/screens/index_view.py +74 -0
  141. tui/screens/init_view.py +82 -0
  142. tui/screens/mcp_view.py +73 -0
  143. tui/screens/optimize_view.py +41 -0
  144. tui/screens/pipe_view.py +46 -0
  145. tui/screens/projects_view.py +355 -0
  146. tui/screens/search_view.py +55 -0
  147. tui/screens/session_view.py +143 -0
  148. tui/screens/stats.py +158 -0
  149. tui/screens/ui_view.py +54 -0
  150. tui/theme.tcss +335 -0
@@ -0,0 +1,585 @@
1
+ """
2
+ Cross-platform background service manager for C3 Project Hub.
3
+
4
+ Windows → Windows Task Scheduler (ONLOGON trigger, pythonw.exe)
5
+ macOS → launchd LaunchAgent (~/.config/LaunchAgents/)
6
+ Linux → systemd user service (~/.config/systemd/user/)
7
+
8
+ Usage:
9
+ svc = HubService()
10
+ svc.status() # {"installed", "running", "platform", "log_path"}
11
+ svc.install(port=3330) # register + immediately start background process
12
+ svc.uninstall() # remove auto-start registration
13
+ svc.start(port=3330) # start background process now
14
+ svc.stop(port=3330) # kill process listening on port
15
+ """
16
+ import json
17
+ import os
18
+ import subprocess
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ try:
23
+ import winreg
24
+ except ImportError:
25
+ winreg = None
26
+
27
+ _C3_PY = Path(__file__).parent.parent / "cli" / "c3.py"
28
+ _LOG_FILE = Path.home() / ".c3" / "hub.log"
29
+
30
+ # ── Windows helpers ───────────────────────────────────────────────────────────
31
+
32
+ def _pythonw() -> str:
33
+ """Return pythonw.exe path (silent, no console window) on Windows."""
34
+ pw = Path(sys.executable).parent / "pythonw.exe"
35
+ return str(pw) if pw.exists() else sys.executable
36
+
37
+
38
+ def _win_startup_dir() -> Path:
39
+ appdata = os.environ.get("APPDATA")
40
+ if appdata:
41
+ return Path(appdata) / "Microsoft" / "Windows" / "Start Menu" / "Programs" / "Startup"
42
+ return (
43
+ Path.home()
44
+ / "AppData"
45
+ / "Roaming"
46
+ / "Microsoft"
47
+ / "Windows"
48
+ / "Start Menu"
49
+ / "Programs"
50
+ / "Startup"
51
+ )
52
+
53
+
54
+ def _vbs_escape(value: str) -> str:
55
+ return value.replace('"', '""')
56
+
57
+
58
+ def _win_reg_registered(task_name: str) -> bool:
59
+ """Check if the hub is registered in the HKCU Run key."""
60
+ if not winreg:
61
+ return False
62
+ try:
63
+ with winreg.OpenKey(
64
+ winreg.HKEY_CURRENT_USER,
65
+ r"Software\Microsoft\Windows\CurrentVersion\Run",
66
+ 0,
67
+ winreg.KEY_READ,
68
+ ) as key:
69
+ winreg.QueryValueEx(key, task_name)
70
+ return True
71
+ except (OSError, FileNotFoundError):
72
+ return False
73
+
74
+
75
+ def _kill_port_win(port: int) -> bool:
76
+ try:
77
+ kwargs = {}
78
+ if sys.platform == "win32":
79
+ kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
80
+ r = subprocess.run(
81
+ f"netstat -ano | findstr :{port}",
82
+ shell=True, capture_output=True, text=True,
83
+ **kwargs
84
+ )
85
+ pids = set()
86
+ for line in r.stdout.strip().splitlines():
87
+ if f":{port}" in line and "LISTENING" in line:
88
+ parts = line.strip().split()
89
+ if len(parts) >= 5:
90
+ pids.add(parts[-1])
91
+ for pid in pids:
92
+ subprocess.run(f"taskkill /PID {pid} /F", shell=True, capture_output=True, **kwargs)
93
+ return bool(pids)
94
+ except Exception:
95
+ return False
96
+
97
+
98
+ def _kill_port_unix(port: int) -> bool:
99
+ try:
100
+ subprocess.run(
101
+ f"lsof -ti:{port} | xargs kill -9",
102
+ shell=True, capture_output=True,
103
+ )
104
+ return True
105
+ except Exception:
106
+ return False
107
+
108
+
109
+ def _make_hub_start_script(repo_root: str, port: int) -> Path:
110
+ """Write ~/.c3/hub_start.py — a self-contained launcher for the background hub.
111
+
112
+ Stored on the local drive so it is always accessible even before network
113
+ drives mount. Sets sys.path itself (no PYTHONPATH needed) and redirects
114
+ all output to hub.log so startup errors are visible.
115
+ """
116
+ script_path = Path.home() / ".c3" / "hub_start.py"
117
+ script_path.parent.mkdir(parents=True, exist_ok=True)
118
+
119
+ escaped = repr(repo_root) # produces 'r"..."' or regular quoted string
120
+ content = (
121
+ "import sys, os, time\n"
122
+ "from pathlib import Path\n"
123
+ "\n"
124
+ f"_REPO = {escaped}\n"
125
+ f"_PORT = {port}\n"
126
+ "_LOG = Path.home() / '.c3' / 'hub.log'\n"
127
+ "\n"
128
+ "# Capture all output so errors are visible in hub.log\n"
129
+ "_LOG.parent.mkdir(parents=True, exist_ok=True)\n"
130
+ "_fh = open(str(_LOG), 'a', encoding='utf-8', buffering=1)\n"
131
+ "sys.stdout = _fh\n"
132
+ "sys.stderr = _fh\n"
133
+ "\n"
134
+ "# Wait up to 60 s for the repo to be accessible (network-drive mounts)\n"
135
+ "for _i in range(12):\n"
136
+ " if Path(_REPO).exists():\n"
137
+ " break\n"
138
+ " time.sleep(5)\n"
139
+ "else:\n"
140
+ " import datetime\n"
141
+ " print(f'[c3-hub] {datetime.datetime.now()} repo not accessible after 60 s: {_REPO}', flush=True)\n"
142
+ " sys.exit(1)\n"
143
+ "\n"
144
+ "sys.path.insert(0, _REPO)\n"
145
+ "os.chdir(_REPO)\n"
146
+ "\n"
147
+ "try:\n"
148
+ " from cli.hub_server import run_hub\n"
149
+ " run_hub(port=_PORT, open_browser=False, silent=True, quiet=True)\n"
150
+ "except Exception as _e:\n"
151
+ " import traceback, datetime\n"
152
+ " print(f'[c3-hub] {datetime.datetime.now()} STARTUP ERROR: {_e}', flush=True)\n"
153
+ " traceback.print_exc(file=_fh)\n"
154
+ )
155
+ script_path.write_text(content, encoding="utf-8")
156
+ return script_path
157
+
158
+
159
+ def _launch_background(port: int):
160
+ """Start hub as a detached background process in quiet background mode."""
161
+ start_script = _make_hub_start_script(str(Path(__file__).parent.parent), port)
162
+ _LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
163
+ exe = _pythonw() if sys.platform == "win32" else sys.executable
164
+ cmd = [exe, str(start_script)]
165
+ kwargs: dict = {}
166
+ if sys.platform == "win32":
167
+ kwargs["creationflags"] = (
168
+ subprocess.DETACHED_PROCESS
169
+ | subprocess.CREATE_NEW_PROCESS_GROUP
170
+ | subprocess.CREATE_NO_WINDOW
171
+ )
172
+ kwargs["close_fds"] = True
173
+ else:
174
+ kwargs["start_new_session"] = True
175
+ subprocess.Popen(cmd, **kwargs)
176
+
177
+
178
+ # ── HubService ────────────────────────────────────────────────────────────────
179
+
180
+ class HubService:
181
+ TASK_NAME = "C3ProjectHub"
182
+ PLIST_LABEL = "com.c3.projecthub"
183
+ SYSTEMD_NAME = "c3hub.service"
184
+
185
+ # ── Public API ────────────────────────────────────────────────────────────
186
+
187
+ def status(self) -> dict:
188
+ if sys.platform == "win32":
189
+ return self._win_status()
190
+ elif sys.platform == "darwin":
191
+ return self._mac_status()
192
+ else:
193
+ return self._linux_status()
194
+
195
+ def install(self, port: int) -> dict:
196
+ if sys.platform == "win32":
197
+ r = self._win_install(port)
198
+ elif sys.platform == "darwin":
199
+ r = self._mac_install(port)
200
+ else:
201
+ r = self._linux_install(port)
202
+ if r.get("success"):
203
+ _launch_background(port)
204
+ return r
205
+
206
+ def uninstall(self) -> dict:
207
+ if sys.platform == "win32":
208
+ return self._win_uninstall()
209
+ elif sys.platform == "darwin":
210
+ return self._mac_uninstall()
211
+ else:
212
+ return self._linux_uninstall()
213
+
214
+ def start(self, port: int) -> dict:
215
+ try:
216
+ _launch_background(port)
217
+ return {"success": True, "output": f"Hub starting on port {port}…"}
218
+ except Exception as e:
219
+ return {"success": False, "output": str(e)}
220
+
221
+ def stop(self, port: int) -> dict:
222
+ try:
223
+ if sys.platform == "win32":
224
+ ok = _kill_port_win(port)
225
+ else:
226
+ ok = _kill_port_unix(port)
227
+ return {"success": ok, "output": f"Killed process on :{port}" if ok else "No process found"}
228
+ except Exception as e:
229
+ return {"success": False, "output": str(e)}
230
+
231
+ # ── Windows ───────────────────────────────────────────────────────────────
232
+
233
+ @property
234
+ def _startup_script_path(self) -> Path:
235
+ return _win_startup_dir() / f"{self.TASK_NAME}.vbs"
236
+
237
+ def _win_task_registered(self) -> bool:
238
+ r = subprocess.run(
239
+ ["schtasks", "/query", "/tn", self.TASK_NAME, "/fo", "LIST"],
240
+ capture_output=True, text=True,
241
+ creationflags=subprocess.CREATE_NO_WINDOW,
242
+ )
243
+ return r.returncode == 0
244
+
245
+ def _win_status(self) -> dict:
246
+ cfg = self._read_hub_config()
247
+ port = cfg.get("port", 3330)
248
+ task_installed = self._win_task_registered()
249
+ reg_installed = _win_reg_registered(self.TASK_NAME)
250
+ startup_installed = self._startup_script_path.exists()
251
+ running = self._is_port_alive(port)
252
+
253
+ if task_installed:
254
+ method = "Windows Task Scheduler (runs at login, no terminal)"
255
+ elif reg_installed:
256
+ method = "Windows Registry Run key (runs at login, silent)"
257
+ elif startup_installed:
258
+ method = "Windows Startup folder — legacy, consider reinstalling"
259
+ else:
260
+ method = "not installed"
261
+
262
+ return {
263
+ "installed": task_installed or reg_installed or startup_installed,
264
+ "running": running,
265
+ "port": port,
266
+ "platform": "windows",
267
+ "log_path": str(_LOG_FILE),
268
+ "method": method,
269
+ }
270
+
271
+ def _is_port_alive(self, port: int) -> bool:
272
+ """Check if anything is listening on the given port."""
273
+ import socket
274
+ try:
275
+ with socket.create_connection(("127.0.0.1", port), timeout=0.1):
276
+ return True
277
+ except Exception:
278
+ return False
279
+
280
+ def _read_hub_config(self) -> dict:
281
+ """Read hub config from ~/.c3/hub_config.json."""
282
+ config_path = Path.home() / ".c3" / "hub_config.json"
283
+ if config_path.exists():
284
+ try:
285
+ with open(config_path, encoding="utf-8") as f:
286
+ return json.load(f)
287
+ except Exception:
288
+ pass
289
+ return {}
290
+
291
+ def _win_reg_install(self, pythonw: str, start_script: Path) -> bool:
292
+ """Register the hub in the HKCU Run key."""
293
+ if not winreg:
294
+ return False
295
+ try:
296
+ with winreg.OpenKey(
297
+ winreg.HKEY_CURRENT_USER,
298
+ r"Software\Microsoft\Windows\CurrentVersion\Run",
299
+ 0,
300
+ winreg.KEY_SET_VALUE,
301
+ ) as key:
302
+ # Command: "pythonw.exe" "hub_start.py"
303
+ cmd = f'"{pythonw}" "{start_script}"'
304
+ winreg.SetValueEx(key, self.TASK_NAME, 0, winreg.REG_SZ, cmd)
305
+ return True
306
+ except OSError:
307
+ return False
308
+
309
+ def _win_reg_uninstall(self) -> bool:
310
+ """Remove the hub from the HKCU Run key."""
311
+ if not winreg:
312
+ return False
313
+ try:
314
+ with winreg.OpenKey(
315
+ winreg.HKEY_CURRENT_USER,
316
+ r"Software\Microsoft\Windows\CurrentVersion\Run",
317
+ 0,
318
+ winreg.KEY_SET_VALUE,
319
+ ) as key:
320
+ winreg.DeleteValue(key, self.TASK_NAME)
321
+ return True
322
+ except (OSError, FileNotFoundError):
323
+ return False
324
+
325
+ def _win_install(self, port: int) -> dict:
326
+ r"""Register the hub for auto-start on Windows.
327
+
328
+ Tries Windows Task Scheduler first (allows 30s delay).
329
+ Falls back to HKCU\...\Run registry key if Task Scheduler fails (e.g. Access Denied).
330
+ """
331
+ pythonw = _pythonw()
332
+ repo_root = str(Path(__file__).parent.parent)
333
+
334
+ # Write the launcher script to the local drive (~/.c3/hub_start.py)
335
+ start_script = _make_hub_start_script(repo_root, port)
336
+
337
+ def _xe(s: str) -> str:
338
+ """Minimal XML attribute/text escaping."""
339
+ return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
340
+
341
+ task_xml = (
342
+ '<?xml version="1.0" encoding="UTF-16"?>\n'
343
+ '<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">\n'
344
+ ' <RegistrationInfo>\n'
345
+ ' <Description>C3 Project Hub background server</Description>\n'
346
+ ' </RegistrationInfo>\n'
347
+ ' <Triggers>\n'
348
+ ' <LogonTrigger>\n'
349
+ ' <Enabled>true</Enabled>\n'
350
+ ' <Delay>PT30S</Delay>\n'
351
+ ' </LogonTrigger>\n'
352
+ ' </Triggers>\n'
353
+ ' <Principals>\n'
354
+ ' <Principal id="Author">\n'
355
+ ' <LogonType>InteractiveToken</LogonType>\n'
356
+ ' <RunLevel>LeastPrivilege</RunLevel>\n'
357
+ ' </Principal>\n'
358
+ ' </Principals>\n'
359
+ ' <Settings>\n'
360
+ ' <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>\n'
361
+ ' <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>\n'
362
+ ' <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>\n'
363
+ ' <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>\n'
364
+ ' <WakeToRun>false</WakeToRun>\n'
365
+ ' </Settings>\n'
366
+ ' <Actions Context="Author">\n'
367
+ ' <Exec>\n'
368
+ f' <Command>{_xe(pythonw)}</Command>\n'
369
+ f' <Arguments>{_xe(str(start_script))}</Arguments>\n'
370
+ ' </Exec>\n'
371
+ ' </Actions>\n'
372
+ '</Task>\n'
373
+ )
374
+
375
+ import tempfile
376
+ tmp_xml = None
377
+ messages = []
378
+ try:
379
+ with tempfile.NamedTemporaryFile(
380
+ mode="w", suffix=".xml", delete=False,
381
+ encoding="utf-16", prefix="c3hub_",
382
+ ) as f:
383
+ f.write(task_xml)
384
+ tmp_xml = f.name
385
+
386
+ # Try Task Scheduler
387
+ r = subprocess.run(
388
+ ["schtasks", "/create", "/tn", self.TASK_NAME, "/xml", tmp_xml, "/f"],
389
+ capture_output=True, text=True,
390
+ creationflags=subprocess.CREATE_NO_WINDOW,
391
+ )
392
+ if r.returncode == 0:
393
+ messages.append(f"Task '{self.TASK_NAME}' registered in Task Scheduler.")
394
+ # Clean up registry if it was there before
395
+ self._win_reg_uninstall()
396
+ else:
397
+ # Fallback to Registry
398
+ if self._win_reg_install(pythonw, start_script):
399
+ messages.append("Task Scheduler failed (Access Denied); registered in Registry Run key instead.")
400
+ else:
401
+ out = (r.stdout + r.stderr).strip()
402
+ return {"success": False, "output": out or "Failed to register via Task Scheduler or Registry."}
403
+
404
+ # Remove legacy Startup-folder VBS if present
405
+ if self._startup_script_path.exists():
406
+ self._startup_script_path.unlink()
407
+ messages.append("Removed legacy startup-folder script.")
408
+
409
+ return {"success": True, "output": "\n".join(messages)}
410
+
411
+ except Exception as e:
412
+ return {"success": False, "output": str(e)}
413
+ finally:
414
+ if tmp_xml:
415
+ try:
416
+ os.unlink(tmp_xml)
417
+ except Exception:
418
+ pass
419
+
420
+ def _win_uninstall(self) -> dict:
421
+ messages = []
422
+ success = True
423
+
424
+ # Remove Task Scheduler task
425
+ if self._win_task_registered():
426
+ r = subprocess.run(
427
+ ["schtasks", "/delete", "/tn", self.TASK_NAME, "/f"],
428
+ capture_output=True, text=True,
429
+ creationflags=subprocess.CREATE_NO_WINDOW,
430
+ )
431
+ out = (r.stdout + r.stderr).strip()
432
+ if r.returncode == 0:
433
+ messages.append("Task Scheduler task removed.")
434
+ else:
435
+ success = False
436
+ messages.append(out or "Failed to remove Task Scheduler task.")
437
+
438
+ # Remove Registry key
439
+ if _win_reg_registered(self.TASK_NAME):
440
+ if self._win_reg_uninstall():
441
+ messages.append("Registry Run key removed.")
442
+ else:
443
+ success = False
444
+ messages.append("Failed to remove Registry Run key.")
445
+
446
+ # Remove the hub_start.py launcher script
447
+ start_script = Path.home() / ".c3" / "hub_start.py"
448
+ if start_script.exists():
449
+ start_script.unlink()
450
+ messages.append("Launcher script removed.")
451
+
452
+ # Remove legacy Startup-folder VBS if still present
453
+ if self._startup_script_path.exists():
454
+ self._startup_script_path.unlink()
455
+ messages.append("Legacy startup-folder script removed.")
456
+
457
+ return {
458
+ "success": success,
459
+ "output": "\n".join(messages) or "No startup registration found.",
460
+ }
461
+
462
+ # ── macOS ─────────────────────────────────────────────────────────────────
463
+
464
+ @property
465
+ def _plist_path(self) -> Path:
466
+ return Path.home() / "Library" / "LaunchAgents" / f"{self.PLIST_LABEL}.plist"
467
+
468
+ def _mac_status(self) -> dict:
469
+ installed = self._plist_path.exists()
470
+ running = None
471
+ if installed:
472
+ r = subprocess.run(
473
+ ["launchctl", "list", self.PLIST_LABEL],
474
+ capture_output=True, text=True,
475
+ )
476
+ running = r.returncode == 0
477
+ return {
478
+ "installed": installed,
479
+ "running": running,
480
+ "platform": "macos",
481
+ "log_path": str(_LOG_FILE),
482
+ "method": "launchd LaunchAgent (RunAtLoad)",
483
+ }
484
+
485
+ def _mac_install(self, port: int) -> dict:
486
+ plist = f"""<?xml version="1.0" encoding="UTF-8"?>
487
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
488
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
489
+ <plist version="1.0"><dict>
490
+ <key>Label</key><string>{self.PLIST_LABEL}</string>
491
+ <key>ProgramArguments</key>
492
+ <array>
493
+ <string>{sys.executable}</string>
494
+ <string>{_C3_PY}</string>
495
+ <string>hub</string>
496
+ <string>--port</string><string>{port}</string>
497
+ <string>--no-browser</string>
498
+ <string>--silent</string>
499
+ <string>--extra-silent</string>
500
+ </array>
501
+ <key>RunAtLoad</key><true/>
502
+ <key>KeepAlive</key><true/>
503
+ <key>StandardOutPath</key><string>{_LOG_FILE}</string>
504
+ <key>StandardErrorPath</key><string>{_LOG_FILE}</string>
505
+ </dict></plist>"""
506
+ self._plist_path.parent.mkdir(parents=True, exist_ok=True)
507
+ self._plist_path.write_text(plist, encoding="utf-8")
508
+ r = subprocess.run(
509
+ ["launchctl", "load", str(self._plist_path)],
510
+ capture_output=True, text=True,
511
+ )
512
+ return {
513
+ "success": r.returncode == 0,
514
+ "output": (r.stdout + r.stderr).strip() or "LaunchAgent loaded.",
515
+ }
516
+
517
+ def _mac_uninstall(self) -> dict:
518
+ if self._plist_path.exists():
519
+ subprocess.run(
520
+ ["launchctl", "unload", str(self._plist_path)],
521
+ capture_output=True,
522
+ )
523
+ self._plist_path.unlink()
524
+ return {"success": True, "output": "LaunchAgent removed."}
525
+
526
+ # ── Linux (systemd user) ──────────────────────────────────────────────────
527
+
528
+ @property
529
+ def _service_path(self) -> Path:
530
+ return (
531
+ Path.home() / ".config" / "systemd" / "user" / self.SYSTEMD_NAME
532
+ )
533
+
534
+ def _linux_status(self) -> dict:
535
+ installed = self._service_path.exists()
536
+ running = None
537
+ if installed:
538
+ r = subprocess.run(
539
+ ["systemctl", "--user", "is-active", self.SYSTEMD_NAME],
540
+ capture_output=True, text=True,
541
+ )
542
+ running = r.stdout.strip() == "active"
543
+ return {
544
+ "installed": installed,
545
+ "running": running,
546
+ "platform": "linux",
547
+ "log_path": str(_LOG_FILE),
548
+ "method": "systemd user service (loginctl linger recommended)",
549
+ }
550
+
551
+ def _linux_install(self, port: int) -> dict:
552
+ unit = (
553
+ "[Unit]\n"
554
+ "Description=C3 Project Hub\n"
555
+ "After=network.target\n\n"
556
+ "[Service]\n"
557
+ f"ExecStart={sys.executable} {_C3_PY} hub --port {port} --no-browser --silent --extra-silent\n"
558
+ "Restart=on-failure\n"
559
+ "RestartSec=5\n"
560
+ f"StandardOutput=append:{_LOG_FILE}\n"
561
+ f"StandardError=append:{_LOG_FILE}\n\n"
562
+ "[Install]\n"
563
+ "WantedBy=default.target\n"
564
+ )
565
+ self._service_path.parent.mkdir(parents=True, exist_ok=True)
566
+ self._service_path.write_text(unit, encoding="utf-8")
567
+ subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True)
568
+ r = subprocess.run(
569
+ ["systemctl", "--user", "enable", "--now", self.SYSTEMD_NAME],
570
+ capture_output=True, text=True,
571
+ )
572
+ return {
573
+ "success": r.returncode == 0,
574
+ "output": (r.stdout + r.stderr).strip() or "Service enabled and started.",
575
+ }
576
+
577
+ def _linux_uninstall(self) -> dict:
578
+ subprocess.run(
579
+ ["systemctl", "--user", "disable", "--now", self.SYSTEMD_NAME],
580
+ capture_output=True,
581
+ )
582
+ if self._service_path.exists():
583
+ self._service_path.unlink()
584
+ subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True)
585
+ return {"success": True, "output": "systemd user service removed."}