cdx-manager 0.17.1 → 0.18.0

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,445 @@
1
+ """Tray domain commands: the `cdx tray` surface a companion talks to.
2
+
3
+ `status` is what the native companion reads. `install`, `launch`, `uninstall`
4
+ and `autostart` act on recorded state and say plainly when there is none, and
5
+ `doctor` reads all of it without changing any of it.
6
+ """
7
+ import os
8
+ import subprocess
9
+ from datetime import datetime, timezone
10
+
11
+ from ..cli_args import TRAY_USAGE, _parse_flag_args
12
+ from ..cli_helpers import _json_success, _write_json
13
+ from ..cli_render import _dim, _pad_table, _style, _warn
14
+ from ..errors import CdxError
15
+ from ..tray_autostart import disable as autostart_disable
16
+ from ..tray_autostart import enable as autostart_enable
17
+ from ..tray_autostart import status as autostart_status
18
+ from ..tray_capability import desktop_capability, toast_capability
19
+ from ..tray_contract import (
20
+ AUTH_LOCKED,
21
+ FRESH,
22
+ UNKNOWN,
23
+ build_snapshot,
24
+ )
25
+ from ..tray_events import (
26
+ acknowledge as ack_events,
27
+ )
28
+ from ..tray_events import (
29
+ read_events,
30
+ write_heartbeat,
31
+ )
32
+ from ..tray_install import (
33
+ TrayInstallError,
34
+ companion_path,
35
+ current_target,
36
+ install,
37
+ interrupted_update,
38
+ launch_command,
39
+ read_state,
40
+ uninstall,
41
+ )
42
+ from ..tray_instance import companion_instance
43
+
44
+ TRAY_ACTIONS = ("status", "install", "launch", "uninstall", "autostart", "doctor", "events", "ack", "heartbeat")
45
+
46
+ # Codes rather than prose, so a caller can branch on them.
47
+ COMPANION_UNAVAILABLE = "tray_companion_not_available"
48
+ COMPANION_NOT_INSTALLED = "tray_companion_not_installed"
49
+ COMPANION_MISSING_FILE = "tray_companion_missing"
50
+ AUTOSTART_UNSUPPORTED = "tray_autostart_unsupported"
51
+
52
+ _FRESHNESS_TEXT = {
53
+ FRESH: "fresh",
54
+ AUTH_LOCKED: "cannot refresh while the session runs",
55
+ UNKNOWN: "never reported",
56
+ }
57
+
58
+
59
+ def handle_tray(rest, ctx):
60
+ if not rest or rest[0] in ("help", "--help", "-h"):
61
+ ctx["out"](f"{TRAY_USAGE}\n")
62
+ return 0
63
+ action, *args = rest
64
+ if action not in TRAY_ACTIONS:
65
+ raise CdxError(TRAY_USAGE)
66
+ if action == "status":
67
+ return _tray_status(args, ctx)
68
+ if action == "launch":
69
+ return _tray_launch(args, ctx)
70
+ if action == "uninstall":
71
+ return _tray_uninstall(args, ctx)
72
+ if action == "autostart":
73
+ return _tray_autostart(args, ctx)
74
+ if action == "doctor":
75
+ return _tray_doctor(args, ctx)
76
+ if action == "events":
77
+ return _tray_events(args, ctx)
78
+ if action == "ack":
79
+ return _tray_ack(args, ctx)
80
+ if action == "heartbeat":
81
+ return _tray_heartbeat(args, ctx)
82
+ return _tray_install(args, ctx)
83
+
84
+
85
+ def _tray_status(args, ctx):
86
+ parsed = _parse_flag_args(args, {
87
+ "--json": {"key": "json", "type": "bool", "default": False},
88
+ "--refresh": {"key": "refresh", "type": "bool", "default": False},
89
+ "--beat": {"key": "beat", "type": "bool", "default": False},
90
+ }, TRAY_USAGE, positionals_key="args", max_positionals=0)
91
+
92
+ # Cached by default: a tray polls, and a live probe would contend on the
93
+ # Codex auth lock and can invalidate a rotating refresh token. Only an
94
+ # explicit refresh, which is a user gesture from the menu, may probe.
95
+ rows = ctx["service"]["get_status_rows"](
96
+ force_refresh=parsed["refresh"],
97
+ cache_only=not parsed["refresh"],
98
+ )
99
+ snapshot = build_snapshot(
100
+ rows,
101
+ datetime.now(timezone.utc),
102
+ ctx["version"],
103
+ refreshable=not any(row.get("active") for row in rows),
104
+ )
105
+ # Pending alerts ride along with the snapshot rather than costing their own
106
+ # call. Across WSL every invocation is a `wsl.exe` crossing measured at
107
+ # 371-401 ms, so a companion asking three times per poll would triple the
108
+ # idle cost adr_005 fixed a budget for. One crossing, everything the tray
109
+ # needs.
110
+ base_dir = ctx["service"]["base_dir"]
111
+ snapshot["events"] = read_events(base_dir)
112
+ # `--beat` is what claims ownership of alerts, so it is opt-in: a caller
113
+ # that merely looks at the status must not make `cdx notify` believe a tray
114
+ # is listening.
115
+ if parsed["beat"]:
116
+ write_heartbeat(base_dir)
117
+ if parsed["json"]:
118
+ _write_json(ctx, _json_success("tray.status", "Built tray snapshot", snapshot=snapshot))
119
+ return 0
120
+ ctx["out"](f"{_format_snapshot(snapshot, use_color=ctx['use_color'])}\n")
121
+ return 0
122
+
123
+
124
+ def _tray_launch(args, ctx):
125
+ parsed = _parse_flag_args(args, {
126
+ "--json": {"key": "json", "type": "bool", "default": False},
127
+ }, TRAY_USAGE, positionals_key="args", max_positionals=0)
128
+ base_dir = ctx["service"]["base_dir"]
129
+ executable, source = companion_path(base_dir, env=ctx.get("env"))
130
+
131
+ if not executable:
132
+ return _refuse(
133
+ ctx, parsed["json"], "tray.launch", COMPANION_NOT_INSTALLED,
134
+ "No tray companion is installed. Run: cdx tray install",
135
+ )
136
+ if not os.path.exists(executable):
137
+ # A recorded path that no longer exists is worth naming: the companion
138
+ # was removed behind CDX's back, and reinstalling is the fix.
139
+ return _refuse(
140
+ ctx, parsed["json"], "tray.launch", COMPANION_MISSING_FILE,
141
+ f"The recorded tray companion is gone: {executable}. Run: cdx tray install",
142
+ )
143
+
144
+ command = launch_command(executable)
145
+ try:
146
+ # Detached on purpose: the tray outlives the command that started it,
147
+ # and cdx must not wait on a process that only exits when the user quits.
148
+ spawn = ctx.get("spawn_detached") or _spawn_detached
149
+ spawn(command)
150
+ except OSError as error:
151
+ raise CdxError(f"Could not start the tray companion: {error}") from error
152
+
153
+ message = f"Started the tray companion from {executable}"
154
+ if parsed["json"]:
155
+ _write_json(ctx, _json_success(
156
+ "tray.launch", message, executable=executable, source=source, command=command,
157
+ ))
158
+ return 0
159
+ ctx["out"](f"{message}\n")
160
+ return 0
161
+
162
+
163
+ def _spawn_detached(command):
164
+ subprocess.Popen( # noqa: S603 (command is CDX-controlled, never user text)
165
+ command,
166
+ stdin=subprocess.DEVNULL,
167
+ stdout=subprocess.DEVNULL,
168
+ stderr=subprocess.DEVNULL,
169
+ start_new_session=True,
170
+ )
171
+
172
+
173
+ def _tray_uninstall(args, ctx):
174
+ parsed = _parse_flag_args(args, {
175
+ "--json": {"key": "json", "type": "bool", "default": False},
176
+ }, TRAY_USAGE, positionals_key="args", max_positionals=0)
177
+ result = uninstall(ctx["service"]["base_dir"])
178
+ if not result:
179
+ # Nothing recorded means nothing to remove. Deleting on a guess is how
180
+ # an uninstaller takes a file it did not put there.
181
+ return _refuse(
182
+ ctx, parsed["json"], "tray.uninstall", COMPANION_NOT_INSTALLED,
183
+ "No tray companion install is recorded, so nothing was removed.",
184
+ )
185
+ message = f"Removed the tray companion ({len(result['removed'])} path(s))"
186
+ if parsed["json"]:
187
+ _write_json(ctx, _json_success(
188
+ "tray.uninstall", message, removed=result["removed"], applied=True,
189
+ ))
190
+ return 0
191
+ ctx["out"](f"{message}\n")
192
+ return 0
193
+
194
+
195
+ def _tray_install(args, ctx):
196
+ parsed = _parse_flag_args(args, {
197
+ "--json": {"key": "json", "type": "bool", "default": False},
198
+ }, TRAY_USAGE, positionals_key="args", max_positionals=0)
199
+ target = current_target()
200
+ if not target:
201
+ return _refuse(
202
+ ctx, parsed["json"], "tray.install", COMPANION_UNAVAILABLE,
203
+ "No tray companion is published for this operating system and architecture.",
204
+ )
205
+ try:
206
+ state = install(
207
+ ctx["service"]["base_dir"],
208
+ ctx["version"],
209
+ download=ctx.get("download_asset"),
210
+ env=ctx.get("env"),
211
+ )
212
+ except TrayInstallError as error:
213
+ # A refusal, not a crash: nothing was written, and the reason is the
214
+ # useful part. Raising keeps the non-zero exit a caller expects.
215
+ raise CdxError(str(error)) from error
216
+
217
+ message = f"Installed the {state['target']} tray companion for CDX {state['cdx_version']}"
218
+ if parsed["json"]:
219
+ _write_json(ctx, _json_success(
220
+ "tray.install", message,
221
+ target=state["target"], executable=state["executable"],
222
+ sha256=state["sha256"], applied=True,
223
+ ))
224
+ return 0
225
+ ctx["out"](f"{message}\n{_dim(state['executable'], ctx['use_color'])}\n")
226
+ return 0
227
+
228
+
229
+ def _tray_autostart(args, ctx):
230
+ parsed = _parse_flag_args(args, {
231
+ "--json": {"key": "json", "type": "bool", "default": False},
232
+ }, TRAY_USAGE, positionals_key="args", max_positionals=1)
233
+ mode = (parsed["args"] or ["status"])[0]
234
+ if mode not in ("on", "off", "status"):
235
+ raise CdxError(TRAY_USAGE)
236
+ env = ctx.get("env")
237
+
238
+ if mode != "status":
239
+ executable, _source = companion_path(ctx["service"]["base_dir"], env=env)
240
+ if mode == "on" and not executable:
241
+ return _refuse(
242
+ ctx, parsed["json"], "tray.autostart", COMPANION_NOT_INSTALLED,
243
+ "No tray companion is installed, so there is nothing to start at login.",
244
+ )
245
+ try:
246
+ if mode == "on":
247
+ autostart_enable(executable, env=env, run=ctx.get("spawn_sync"))
248
+ else:
249
+ autostart_disable(env=env, run=ctx.get("spawn_sync"))
250
+ except NotImplementedError as error:
251
+ return _refuse(
252
+ ctx, parsed["json"], "tray.autostart", AUTOSTART_UNSUPPORTED, str(error),
253
+ )
254
+
255
+ # Read the platform back rather than reporting what we just intended: a
256
+ # recorded intention that drifted from the system is the confusion doctor
257
+ # exists to end.
258
+ state = autostart_status(env=env, run=ctx.get("spawn_sync"))
259
+ if not state["supported"]:
260
+ return _refuse(
261
+ ctx, parsed["json"], "tray.autostart", AUTOSTART_UNSUPPORTED,
262
+ "This platform has no autostart mechanism CDX knows how to manage.",
263
+ )
264
+ message = f"Tray autostart is {'on' if state['enabled'] else 'off'}"
265
+ if parsed["json"]:
266
+ _write_json(ctx, _json_success(
267
+ "tray.autostart", message,
268
+ enabled=state["enabled"], artifact=state["artifact"], applied=mode != "status",
269
+ ))
270
+ return 0
271
+ ctx["out"](f"{message}\n{_dim(state['artifact'], ctx['use_color'])}\n")
272
+ return 0
273
+
274
+
275
+ # `None` is a real answer here: the bus could not be asked. Reporting it as
276
+ # "no tray" would be a claim the probe did not make.
277
+ _DESKTOP_STATE = {True: "ready", False: "no tray", None: "unknown"}
278
+
279
+
280
+ def _tray_doctor(args, ctx):
281
+ """Every state I had to diagnose by hand, in one bounded report.
282
+
283
+ No background service and no repair: it reads, it never fixes. A doctor that
284
+ also acted would have to be trusted with the machine, and this one only has
285
+ to be trusted to tell the truth.
286
+ """
287
+ parsed = _parse_flag_args(args, {
288
+ "--json": {"key": "json", "type": "bool", "default": False},
289
+ }, TRAY_USAGE, positionals_key="args", max_positionals=0)
290
+ base_dir = ctx["service"]["base_dir"]
291
+ env = ctx.get("env")
292
+ executable, source = companion_path(base_dir, env=env)
293
+ state = read_state(base_dir)
294
+ autostart = autostart_status(env=env, run=ctx.get("spawn_sync"))
295
+ instance = companion_instance()
296
+ desktop = desktop_capability(env=env)
297
+ toasts = toast_capability(env=env)
298
+
299
+ checks = [
300
+ ("companion", "installed" if state else ("override" if executable else "absent"),
301
+ executable or "run: cdx tray install"),
302
+ ("executable", "present" if executable and os.path.exists(executable) else "missing",
303
+ executable or "-"),
304
+ ("cdx_version", ctx["version"], (state or {}).get("cdx_version") or "-"),
305
+ ("target", (state or {}).get("target") or "-", (state or {}).get("sha256") or "-"),
306
+ ("running", "yes" if instance.get("pid") else "no", str(instance.get("pid") or "-")),
307
+ ("autostart", "on" if autostart["enabled"] else "off", autostart["artifact"] or "unsupported"),
308
+ ("update", "interrupted" if interrupted_update(base_dir) else "clean",
309
+ "a staged companion was never promoted; run: cdx tray install"
310
+ if interrupted_update(base_dir) else "-"),
311
+ # Both fail silently when absent, which is the only reason they are
312
+ # worth a line: nothing else would ever tell the user.
313
+ ("desktop", _DESKTOP_STATE.get(desktop.get("available")), desktop["detail"]),
314
+ ("toasts", "ready" if toasts["present"] else ("blocked" if toasts["gated"] else "n/a"),
315
+ toasts["detail"]),
316
+ ]
317
+ if parsed["json"]:
318
+ _write_json(ctx, _json_success(
319
+ "tray.doctor", "Collected tray diagnostics",
320
+ checks=[{"check": c, "state": s, "detail": d} for c, s, d in checks],
321
+ ))
322
+ return 0
323
+ rows = [["CHECK", "STATE", "DETAIL"], *[[c, s, d] for c, s, d in checks]]
324
+ ctx["out"](f"{_pad_table(rows)}\n")
325
+ return 0
326
+
327
+
328
+ def _refuse(ctx, as_json, action, code, message):
329
+ """Report that nothing happened, and why, without failing the command.
330
+
331
+ Not an error: asking to launch a tray that is not installed is a reasonable
332
+ thing to do, and the answer is a state rather than a fault.
333
+ """
334
+ if as_json:
335
+ _write_json(ctx, _json_success(
336
+ action, message, warnings=[{"code": code, "message": message}], applied=False,
337
+ ))
338
+ return 0
339
+ ctx["out"](f"{_warn(message, ctx['use_color'])}\n")
340
+ return 0
341
+
342
+
343
+ def _companion_action(action, args, ctx):
344
+ parsed = _parse_flag_args(args, {
345
+ "--json": {"key": "json", "type": "bool", "default": False},
346
+ }, TRAY_USAGE, positionals_key="args", max_positionals=0)
347
+ message = (
348
+ f"cdx tray {action} is defined but no tray companion ships with CDX {ctx['version']}. "
349
+ "Nothing was installed, started, or changed."
350
+ )
351
+ if parsed["json"]:
352
+ _write_json(ctx, _json_success(
353
+ f"tray.{action}",
354
+ message,
355
+ warnings=[{"code": COMPANION_UNAVAILABLE, "message": message}],
356
+ applied=False,
357
+ ))
358
+ return 0
359
+ ctx["out"](f"{_warn(message, ctx['use_color'])}\n")
360
+ return 0
361
+
362
+
363
+ def _pct(value):
364
+ return "?" if value is None else f"{int(value)}%"
365
+
366
+
367
+ def _format_snapshot(snapshot, use_color=False):
368
+ icon = snapshot["icon"]
369
+ header = _style(f"tray: {icon['state']}", "1", use_color)
370
+ if not snapshot["sessions"]:
371
+ return f"{header}\n{_dim('No enabled sessions.', use_color)}"
372
+ rows = [["SESSION", "PROVIDER", "LEFT", "STATE", "DATA"]]
373
+ for session in snapshot["sessions"]:
374
+ rows.append([
375
+ session["name"] or "?",
376
+ session["provider"] or "-",
377
+ _pct(session["available_pct"]),
378
+ session["state"],
379
+ _FRESHNESS_TEXT.get(session["freshness"], session["freshness"]),
380
+ ])
381
+ lines = [header, _pad_table(rows)]
382
+ if not snapshot["refreshable"]:
383
+ lines.append(_dim("A running session holds the provider lock; refresh will not update it.", use_color))
384
+ return "\n".join(lines)
385
+
386
+
387
+ # The companion reads and acknowledges events through these three commands
388
+ # rather than through the filesystem, and that is the whole point of them: on a
389
+ # Windows host serving CDX from WSL, the spool lives in the Linux filesystem
390
+ # while the tray runs on Windows. Going through `cdx` means the existing
391
+ # `wsl.exe` transport carries events too — no path translation, no share, no
392
+ # socket, and no assumption that either side can see the other's disk.
393
+ def _tray_events(args, ctx):
394
+ parsed = _parse_flag_args(args, {
395
+ "--json": {"key": "json", "type": "bool", "default": False},
396
+ }, TRAY_USAGE, positionals_key="args", max_positionals=0)
397
+ events = read_events(ctx["service"]["base_dir"])
398
+ if parsed["json"]:
399
+ _write_json(ctx, _json_success("tray.events", f"Read {len(events)} pending event(s)", events=events))
400
+ return 0
401
+ if not events:
402
+ ctx["out"]("No pending tray events.\n")
403
+ return 0
404
+ rows = [["WHEN", "KIND", "TITLE", "MESSAGE"]]
405
+ for event in events:
406
+ rows.append([
407
+ datetime.fromtimestamp(event.get("at", 0), timezone.utc).strftime("%H:%M:%S"),
408
+ event.get("kind", "-"), event.get("title", "-"), event.get("message", "-"),
409
+ ])
410
+ ctx["out"](f"{_pad_table(rows)}\n")
411
+ return 0
412
+
413
+
414
+ def _tray_ack(args, ctx):
415
+ parsed = _parse_flag_args(args, {
416
+ "--json": {"key": "json", "type": "bool", "default": False},
417
+ }, TRAY_USAGE, positionals_key="args", max_positionals=64)
418
+ ids = parsed["args"]
419
+ if not ids:
420
+ raise CdxError(TRAY_USAGE)
421
+ result = ack_events(ctx["service"]["base_dir"], ids)
422
+ message = f"Acknowledged {result['acknowledged']} event(s)"
423
+ if parsed["json"]:
424
+ _write_json(ctx, _json_success("tray.ack", message, **result))
425
+ return 0
426
+ ctx["out"](f"{message}\n")
427
+ return 0
428
+
429
+
430
+ def _tray_heartbeat(args, ctx):
431
+ """The companion saying it is alive, so `cdx notify` stops delivering directly.
432
+
433
+ Written by the tray once per poll. `cdx notify` publishes only while this is
434
+ fresh, so a companion that stops beating hands alerts straight back to the
435
+ direct path rather than swallowing them.
436
+ """
437
+ parsed = _parse_flag_args(args, {
438
+ "--json": {"key": "json", "type": "bool", "default": False},
439
+ }, TRAY_USAGE, positionals_key="args", max_positionals=0)
440
+ write_heartbeat(ctx["service"]["base_dir"])
441
+ if parsed["json"]:
442
+ _write_json(ctx, _json_success("tray.heartbeat", "Recorded the tray heartbeat"))
443
+ return 0
444
+ ctx["out"]("Recorded the tray heartbeat.\n")
445
+ return 0
package/src/notify.py CHANGED
@@ -155,9 +155,14 @@ def _run_notification_command(argv, spawn_sync, env):
155
155
  pass
156
156
 
157
157
 
158
- # Windows only shows a toast on behalf of an application it knows. Registering our
159
- # own identifier would mean installing a Start Menu shortcut, so we borrow the one
160
- # PowerShell already ships with; an unknown identifier is dropped in silence.
158
+ # Windows shows a toast only on behalf of an application it knows, and it knows
159
+ # an application by a Start Menu shortcut carrying its AppUserModelID. An
160
+ # unknown identifier is dropped in silence — no error, no toast.
161
+ #
162
+ # PowerShell's own identifier is the fallback, and it is a real compromise: the
163
+ # toast then says "Windows PowerShell", and turning CDX alerts off in Settings
164
+ # means turning PowerShell off. It is used only while CDX has no shortcut of its
165
+ # own, which `cdx tray install` now writes.
161
166
  _POWERSHELL_AUMID = "{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe"
162
167
 
163
168
  _TOAST_SCRIPT = """
@@ -183,7 +188,7 @@ def _send_windows_notification(title, message, spawn_sync, env, launcher):
183
188
  script = _TOAST_SCRIPT.format(
184
189
  title=_escape_powershell(title),
185
190
  message=_escape_powershell(message),
186
- aumid=_POWERSHELL_AUMID,
191
+ aumid=_escape_powershell(windows_aumid(env)),
187
192
  )
188
193
  _run_notification_command(
189
194
  [*launcher, "-NoProfile", "-NonInteractive", "-Command", script],
@@ -192,6 +197,21 @@ def _send_windows_notification(title, message, spawn_sync, env, launcher):
192
197
  )
193
198
 
194
199
 
200
+ def windows_aumid(env=None):
201
+ """CDX's own identifier when Windows can resolve it, PowerShell's otherwise.
202
+
203
+ The shortcut has to exist before the identifier means anything: Windows
204
+ resolves an AppUserModelID through the Start Menu, so claiming ours without
205
+ one would drop every toast in silence — strictly worse than a toast
206
+ attributed to PowerShell.
207
+ """
208
+ from .tray_shortcut import APP_USER_MODEL_ID, shortcut_path
209
+ path = shortcut_path(env)
210
+ if path and os.path.isfile(path):
211
+ return APP_USER_MODEL_ID
212
+ return _POWERSHELL_AUMID
213
+
214
+
195
215
  def _escape_powershell(value):
196
216
  return str(value).replace("'", "''")
197
217
 
@@ -0,0 +1,108 @@
1
+ """Start the tray companion at login, or do not. Explicitly, either way.
2
+
3
+ `req_038` AC1 is the whole design here: installing must never enable startup,
4
+ and turning it on must be a separate, reversible, idempotent act that reports
5
+ what the platform actually holds rather than what CDX last intended.
6
+
7
+ One recorded artifact per platform, chosen for being the plainest thing that can
8
+ be deleted again: a LaunchAgent plist, the per-user Run key, an XDG autostart
9
+ entry. All three are per-user and need no administrator rights.
10
+ """
11
+ import os
12
+ import platform
13
+ import plistlib
14
+ import subprocess
15
+
16
+ LABEL = "com.cdx.tray"
17
+ RUN_KEY = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run"
18
+ RUN_VALUE = "CDXTray"
19
+
20
+
21
+ def _home(env):
22
+ return env.get("HOME") or os.path.expanduser("~")
23
+
24
+
25
+ def artifact_path(env=None, system=None):
26
+ """Where this platform records "start at login", or None if unsupported."""
27
+ env = os.environ if env is None else env
28
+ system = system or platform.system()
29
+ if system == "Darwin":
30
+ return os.path.join(_home(env), "Library", "LaunchAgents", f"{LABEL}.plist")
31
+ if system == "Linux":
32
+ base = env.get("XDG_CONFIG_HOME") or os.path.join(_home(env), ".config")
33
+ return os.path.join(base, "autostart", "cdx-tray.desktop")
34
+ if system == "Windows":
35
+ # Not a path: the registry value is the artifact. Named so callers can
36
+ # report it, and so uninstall knows what to remove.
37
+ return f"{RUN_KEY}\\{RUN_VALUE}"
38
+ return None
39
+
40
+
41
+ def status(env=None, system=None, run=None):
42
+ """What the platform holds right now, read back rather than remembered.
43
+
44
+ Reading the real state is the point: a recorded intention that drifted from
45
+ the system is exactly the confusion `cdx tray doctor` exists to end.
46
+ """
47
+ env = os.environ if env is None else env
48
+ system = system or platform.system()
49
+ path = artifact_path(env=env, system=system)
50
+ if not path:
51
+ return {"supported": False, "enabled": False, "artifact": None}
52
+ if system == "Windows":
53
+ runner = run or _run
54
+ result = runner(["reg", "query", RUN_KEY, "/v", RUN_VALUE])
55
+ return {"supported": True, "enabled": result.returncode == 0, "artifact": path}
56
+ return {"supported": True, "enabled": os.path.exists(path), "artifact": path}
57
+
58
+
59
+ def _run(command):
60
+ return subprocess.run(command, capture_output=True, text=True, check=False)
61
+
62
+
63
+ def enable(executable, env=None, system=None, run=None):
64
+ """Idempotent by construction: writing the same artifact twice is a no-op."""
65
+ env = os.environ if env is None else env
66
+ system = system or platform.system()
67
+ path = artifact_path(env=env, system=system)
68
+ if not path:
69
+ raise NotImplementedError(f"No autostart mechanism for {system}.")
70
+ if system == "Windows":
71
+ runner = run or _run
72
+ runner(["reg", "add", RUN_KEY, "/v", RUN_VALUE, "/t", "REG_SZ", "/d", executable, "/f"])
73
+ return path
74
+ os.makedirs(os.path.dirname(path), exist_ok=True)
75
+ if system == "Darwin":
76
+ with open(path, "wb") as handle:
77
+ plistlib.dump({
78
+ "Label": LABEL,
79
+ "ProgramArguments": [executable],
80
+ "RunAtLoad": True,
81
+ # No KeepAlive: quitting the tray from its own menu must mean
82
+ # quit, not "be restarted immediately by launchd".
83
+ "KeepAlive": False,
84
+ }, handle)
85
+ else:
86
+ with open(path, "w", encoding="utf-8") as handle:
87
+ handle.write(
88
+ "[Desktop Entry]\nType=Application\nName=CDX Tray\n"
89
+ f"Exec={executable}\nX-GNOME-Autostart-enabled=true\n"
90
+ )
91
+ return path
92
+
93
+
94
+ def disable(env=None, system=None, run=None):
95
+ """Remove the artifact. Absent is success, not an error."""
96
+ env = os.environ if env is None else env
97
+ system = system or platform.system()
98
+ path = artifact_path(env=env, system=system)
99
+ if not path:
100
+ return False
101
+ if system == "Windows":
102
+ runner = run or _run
103
+ result = runner(["reg", "delete", RUN_KEY, "/v", RUN_VALUE, "/f"])
104
+ return result.returncode == 0
105
+ if os.path.exists(path):
106
+ os.remove(path)
107
+ return True
108
+ return False