devops-bot-sdk 1.4.146__tar.gz → 1.4.147__tar.gz

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 (95) hide show
  1. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/__init__.py +2 -2
  4. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/ui/service.py +172 -0
  5. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/updater.py +36 -1
  6. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/README.md +0 -0
  7. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
  8. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  9. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  10. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/devops_bot_sdk.egg-info/requires.txt +0 -0
  11. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  12. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/pyproject.toml +0 -0
  13. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/agentd.py +0 -0
  14. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/agents/__init__.py +0 -0
  15. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/agents/builder.py +0 -0
  16. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/agents/chat.py +0 -0
  17. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/agents/connectors.py +0 -0
  18. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/agents/engines/__init__.py +0 -0
  19. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/agents/engines/claude_code.py +0 -0
  20. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/agents/engines/http.py +0 -0
  21. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/agents/engines/mcp.py +0 -0
  22. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/agents/engines/n8n.py +0 -0
  23. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/agents/engines/shell.py +0 -0
  24. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/agents/integrations.py +0 -0
  25. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/agents/inventory.py +0 -0
  26. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/agents/library/__init__.py +0 -0
  27. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/agents/manifest.py +0 -0
  28. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/agents/prompt_studio.py +0 -0
  29. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/agents/prompt_template.py +0 -0
  30. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/agents/prompt_vars.py +0 -0
  31. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/agents/run_log.py +0 -0
  32. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/agents/secrets.py +0 -0
  33. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/agents/usage.py +0 -0
  34. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/agents/varstore.py +0 -0
  35. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/cli.py +0 -0
  36. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/client.py +0 -0
  37. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/collectors/__init__.py +0 -0
  38. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/collectors/files.py +0 -0
  39. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/collectors/process.py +0 -0
  40. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/collectors/screenshot.py +0 -0
  41. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/config.py +0 -0
  42. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/crucial.py +0 -0
  43. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/design_verify/__init__.py +0 -0
  44. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/design_verify/assets.py +0 -0
  45. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/design_verify/bootstrap.py +0 -0
  46. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/design_verify/browser.py +0 -0
  47. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/design_verify/compare.py +0 -0
  48. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/design_verify/loop.py +0 -0
  49. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/exceptions.py +0 -0
  50. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/git_ops.py +0 -0
  51. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/graphify.py +0 -0
  52. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/hooks/__init__.py +0 -0
  53. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/hooks/crucial_guard.py +0 -0
  54. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/ipc/__init__.py +0 -0
  55. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/ipc/electron_bridge.py +0 -0
  56. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/ipc/handlers.py +0 -0
  57. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/local_exec.py +0 -0
  58. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/models/__init__.py +0 -0
  59. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/models/envelope.py +0 -0
  60. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/models/requests.py +0 -0
  61. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/models/responses.py +0 -0
  62. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/models/snapshots.py +0 -0
  63. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/platform_compat.py +0 -0
  64. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/py.typed +0 -0
  65. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/run_auto.py +0 -0
  66. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/sse.py +0 -0
  67. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/test.py +0 -0
  68. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/test_pipeline.py +0 -0
  69. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/ui/__init__.py +0 -0
  70. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/ui/api.py +0 -0
  71. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/ui/dashboard.py +0 -0
  72. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/ui/health.py +0 -0
  73. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/ui/ip_allowlist.py +0 -0
  74. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/ui/jobs.py +0 -0
  75. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/ui/models.py +0 -0
  76. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/ui/runner.py +0 -0
  77. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/ui/security.py +0 -0
  78. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/ui/server.py +0 -0
  79. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/ui/setup_manager.py +0 -0
  80. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/ui/static/agents.html +0 -0
  81. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/ui/static/app.js +0 -0
  82. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/ui/static/custom-agents.html +0 -0
  83. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/ui/static/default-agents.html +0 -0
  84. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/ui/static/index.html +0 -0
  85. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/ui/static/runs.html +0 -0
  86. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/ui/static/setup.html +0 -0
  87. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/ui/static/styles.css +0 -0
  88. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/sdk/ui/static/tickets.html +0 -0
  89. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/setup.cfg +0 -0
  90. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/tests/test_agent_chat_features.py +0 -0
  91. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/tests/test_agent_prompt_models.py +0 -0
  92. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/tests/test_agents_connectors_builder.py +0 -0
  93. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/tests/test_git_action.py +0 -0
  94. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/tests/test_health.py +0 -0
  95. {devops_bot_sdk-1.4.146 → devops_bot_sdk-1.4.147}/tests/test_updater_extras.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.146
3
+ Version: 1.4.147
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.146
3
+ Version: 1.4.147
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.4.146
3
+ Version: 1.4.147
4
4
 
5
5
  Public surface:
6
6
  BackendClient.from_config() — create client from ~/.agentos/config.toml
@@ -30,7 +30,7 @@ Rules:
30
30
  - All data egress through submit_webhook only
31
31
  """
32
32
 
33
- __version__ = "1.4.146" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
33
+ __version__ = "1.4.147" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
34
34
  __author__ = "AgentOS" # sdk.client.SDK_VERSION and the UI server version all read this.
35
35
 
36
36
  from sdk.client import BackendClient
@@ -17,6 +17,7 @@ Everything is best-effort and returns ``(ok, message)`` so the caller (the
17
17
  from __future__ import annotations
18
18
 
19
19
  import os
20
+ import re
20
21
  import shutil
21
22
  import subprocess
22
23
  import sys
@@ -285,3 +286,174 @@ def restart() -> tuple[bool, str]:
285
286
  return (rc == 0), (f"restarted systemd service '{SERVICE_NAME}'." if rc == 0
286
287
  else out or f"systemctl restart {SERVICE_NAME} failed")
287
288
  return False, "no supported service manager found — restart the panel manually (`agentos-ui`)."
289
+
290
+
291
+ def _systemd_unit_candidates() -> list[Path]:
292
+ """Both places a systemd unit can live, system scope first."""
293
+ return [
294
+ Path("/etc/systemd/system") / f"{SERVICE_NAME}.service",
295
+ Path.home() / ".config" / "systemd" / "user" / f"{SERVICE_NAME}.service",
296
+ ]
297
+
298
+
299
+ def _existing_systemd_unit() -> Path | None:
300
+ """The installed unit file, checking BOTH system and user scope regardless of
301
+ the current privilege — so a non-root ``agentos-auto update`` still finds a
302
+ system service installed via ``sudo agentos-ui install`` (and vice-versa)."""
303
+ for p in _systemd_unit_candidates():
304
+ try:
305
+ if p.exists():
306
+ return p
307
+ except OSError:
308
+ continue
309
+ return None
310
+
311
+
312
+ def _service_definition_path() -> Path:
313
+ """On-disk service definition — an EXISTING unit if there is one (either scope),
314
+ else the path we would CREATE at the current privilege."""
315
+ if IS_WINDOWS: # schtasks has no file; handled separately
316
+ return Path()
317
+ if IS_MAC:
318
+ return _mac_plist_path()
319
+ existing = _existing_systemd_unit()
320
+ if existing:
321
+ return existing
322
+ is_root = hasattr(os, "geteuid") and os.geteuid() == 0
323
+ base = Path("/etc/systemd/system") if is_root else Path.home() / ".config" / "systemd" / "user"
324
+ return base / f"{SERVICE_NAME}.service"
325
+
326
+
327
+ def _needs_root_for_existing() -> bool:
328
+ """True when a SYSTEM unit is installed but we're not root — so we must not
329
+ silently create a conflicting *user* unit; the caller should ask for sudo."""
330
+ if IS_WINDOWS or IS_MAC:
331
+ return False
332
+ is_root = hasattr(os, "geteuid") and os.geteuid() == 0
333
+ existing = _existing_systemd_unit()
334
+ return (existing is not None
335
+ and existing.parent == Path("/etc/systemd/system")
336
+ and not is_root)
337
+
338
+
339
+ def is_installed() -> bool:
340
+ """True when a managed agentos-ui service/unit exists (any platform, any scope).
341
+
342
+ Best-effort and never raises — used by the updater to decide whether an upgrade
343
+ should create, regenerate, or restart the service.
344
+ """
345
+ try:
346
+ if IS_WINDOWS:
347
+ rc, _ = _run(["schtasks", "/Query", "/TN", WIN_TASK])
348
+ return rc == 0
349
+ if IS_MAC:
350
+ return _mac_plist_path().exists()
351
+ return _existing_systemd_unit() is not None
352
+ except Exception: # noqa: BLE001
353
+ return False
354
+
355
+
356
+ def _is_active() -> bool:
357
+ """True when the managed service is currently running (read-only query)."""
358
+ try:
359
+ if IS_WINDOWS:
360
+ _, out = _run(["schtasks", "/Query", "/TN", WIN_TASK, "/FO", "LIST", "/V"])
361
+ return "Running" in out
362
+ if IS_MAC:
363
+ rc, out = _run(["launchctl", "list", _MAC_LABEL])
364
+ return rc == 0 and bool(out.strip())
365
+ existing = _existing_systemd_unit()
366
+ user = existing is not None and existing.parent.name == "user"
367
+ sysctl = ["systemctl", "--user"] if user else ["systemctl"]
368
+ _, out = _run([*sysctl, "is-active", SERVICE_NAME])
369
+ return out.strip() == "active"
370
+ except Exception: # noqa: BLE001
371
+ return False
372
+
373
+
374
+ def _installed_host_port() -> tuple[str, int]:
375
+ """Read the host/port the service was installed with, so a refresh preserves
376
+ them instead of resetting to the install() defaults. Falls back to 0.0.0.0:8765.
377
+ """
378
+ host, port = "0.0.0.0", 8765
379
+ text = ""
380
+ try:
381
+ if IS_WINDOWS:
382
+ _, text = _run(["schtasks", "/Query", "/TN", WIN_TASK, "/V", "/FO", "LIST"])
383
+ else:
384
+ p = _service_definition_path()
385
+ text = p.read_text() if p and p.exists() else ""
386
+ except Exception: # noqa: BLE001
387
+ text = ""
388
+ clean = re.sub(r"<[^>]+>", " ", text) # flatten plist <string> tags to spaces
389
+ m = re.search(r"--host\s+(\S+)", clean)
390
+ if m:
391
+ host = m.group(1).strip().strip('"')
392
+ m = re.search(r"--port\s+(\d+)", clean)
393
+ if m:
394
+ port = int(m.group(1))
395
+ return host, port
396
+
397
+
398
+ def refresh() -> tuple[bool, str]:
399
+ """Regenerate the service definition and restart it after an SDK upgrade.
400
+
401
+ ``agentos-auto update`` only pip-upgrades the package; the on-disk unit and the
402
+ running process are untouched. So a change to the generated unit (e.g. the
403
+ baked-in PATH that lets the service find the ``claude`` CLI) or to the code
404
+ never takes effect until this runs. Regenerating rewrites the unit with the
405
+ upgraded SDK's definition (preserving the installed host/port), and the restart
406
+ both applies the new unit and loads the new code.
407
+
408
+ No-op returning ``(False, msg)`` when no managed service is installed, so a
409
+ manually-launched panel is left alone.
410
+ """
411
+ if not is_installed():
412
+ return False, "no managed service installed — nothing to refresh."
413
+ if _needs_root_for_existing():
414
+ return False, ("the panel runs as a system service — re-run with sudo "
415
+ "(`sudo agentos-ui install`) to apply the update.")
416
+ host, port = _installed_host_port()
417
+ ok, msg = install(host, port) # rewrites unit/plist/task + daemon-reload
418
+ if not ok:
419
+ return False, f"could not regenerate service: {msg}"
420
+ # `enable --now` / launchctl load won't restart an ALREADY-running unit, so
421
+ # force a restart to apply the regenerated definition and load the new code.
422
+ rok, rmsg = restart()
423
+ return (True, f"service refreshed ({host}:{port}); {rmsg}" if rok
424
+ else f"service definition regenerated ({host}:{port}) but restart needs attention: {rmsg}")
425
+
426
+
427
+ def _service_disabled() -> bool:
428
+ return os.environ.get("AGENTOS_NO_SERVICE", "").strip().lower() in ("1", "true", "yes", "on")
429
+
430
+
431
+ def ensure(restart_running: bool = False) -> tuple[bool, str]:
432
+ """Make sure the panel service EXISTS and is running the current SDK.
433
+
434
+ This is what lets ``agentos-auto update`` yield a working 24/7 service without a
435
+ separate ``agentos-ui install`` step:
436
+
437
+ * not installed → create it (``install``)
438
+ * installed + code changed → ``refresh`` (regenerate unit + restart)
439
+ (pass ``restart_running=True`` after a real upgrade)
440
+ * installed + unchanged + running → leave it alone
441
+ * installed + unchanged + stopped → start it
442
+
443
+ Best-effort; opt out entirely with ``AGENTOS_NO_SERVICE=1`` (e.g. a box that
444
+ runs the panel manually or only uses the SDK as a library).
445
+ """
446
+ if _service_disabled():
447
+ return False, "AGENTOS_NO_SERVICE set — leaving the service unmanaged."
448
+ if not is_installed():
449
+ ok, msg = install() # create at the current privilege
450
+ return ok, (f"service created — {msg}" if ok else f"could not create service: {msg}")
451
+ if _needs_root_for_existing():
452
+ return False, ("a system service is installed — run `sudo agentos-ui install` "
453
+ "to apply updates and manage it.")
454
+ if restart_running:
455
+ return refresh()
456
+ if _is_active():
457
+ return True, "service already running."
458
+ rok, rmsg = restart() # installed but down → bring it back up
459
+ return rok, (f"service was down — started; {rmsg}" if rok else rmsg)
@@ -273,6 +273,37 @@ def self_update() -> bool:
273
273
  return False
274
274
 
275
275
 
276
+ def _ensure_service(quiet: bool = False, *, restart: bool = False) -> None:
277
+ """Ensure the panel service exists and runs the current SDK, after an update check.
278
+
279
+ Without this, ``agentos-auto update`` pip-upgrades the code but leaves the old
280
+ systemd unit / launchd plist and the old running process in place — so unit
281
+ changes (e.g. the baked-in PATH that lets the service find ``claude``) and new
282
+ code silently never take effect, and a box that never ran ``agentos-ui install``
283
+ has no service at all. ``service.ensure`` creates it when missing, refreshes it
284
+ after a real upgrade (``restart=True``), and starts it if it's down — so a plain
285
+ ``agentos-auto update`` reliably leaves ``systemctl status agentos-ui`` running.
286
+
287
+ Best-effort and never raises. Silent on a box without the ``[ui]`` extra (no
288
+ panel to manage) and opt-out via ``AGENTOS_NO_SERVICE=1``.
289
+
290
+ Safe here because ``auto_update`` runs from the short-lived ``agentos-auto``
291
+ process, not inside the long-running panel — restarting the panel can't kill
292
+ the updater mid-run.
293
+ """
294
+ try:
295
+ from sdk.ui import service
296
+ except Exception: # noqa: BLE001 — no [ui] extra installed → no panel service
297
+ return
298
+ try:
299
+ ok, msg = service.ensure(restart_running=restart)
300
+ if not quiet and msg:
301
+ print(f"↻ {msg}" if ok else f"(service: {msg})")
302
+ except Exception as exc: # noqa: BLE001 — an update must never fail on the service step
303
+ if not quiet:
304
+ print(f"(service check failed: {exc})")
305
+
306
+
276
307
  def auto_update(quiet: bool = False) -> tuple[bool, str, str | None]:
277
308
  """Check for a newer version and upgrade if one exists.
278
309
 
@@ -292,13 +323,16 @@ def auto_update(quiet: bool = False) -> tuple[bool, str, str | None]:
292
323
  if not is_github:
293
324
  latest = latest_pypi_version()
294
325
  if not latest or _vtuple(latest) <= _vtuple(cur):
326
+ _ensure_service(quiet, restart=False) # already current — just self-heal the service
295
327
  return (False, cur, latest)
296
328
  if not quiet:
297
329
  print(f"updating {_DIST} {cur} → {latest} …")
298
330
  ok = self_update()
299
331
  if not quiet and ok:
300
332
  print("✓ updated.")
301
- # silent on failure — continue on the existing version without noise
333
+ # Ensure the service exists + runs the new code (restart only on a real
334
+ # upgrade); also self-heals a missing/down service when already current.
335
+ _ensure_service(quiet, restart=ok)
302
336
  return (ok, cur, latest if ok else None)
303
337
 
304
338
  # GitHub path
@@ -313,6 +347,7 @@ def auto_update(quiet: bool = False) -> tuple[bool, str, str | None]:
313
347
  elif ok:
314
348
  print(f"✓ already up to date (v{cur}).")
315
349
  # silent on failure — continue on the existing version without noise
350
+ _ensure_service(quiet, restart=updated)
316
351
  return (updated, cur, after if updated else None)
317
352
 
318
353