Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31e9ab8357 | ||
|
|
8c2a3f835d |
@@ -34,7 +34,6 @@ Using context manager:
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
from typing import Optional
|
||||
|
||||
@@ -140,13 +139,17 @@ class HindsightEmbedded:
|
||||
return
|
||||
|
||||
if self._closed:
|
||||
raise RuntimeError("Cannot use HindsightEmbedded after it has been closed")
|
||||
raise RuntimeError(
|
||||
"Cannot use HindsightEmbedded after it has been closed"
|
||||
)
|
||||
|
||||
# Use embed manager interface for daemon management
|
||||
logger.info(f"Ensuring daemon is running for profile '{self.profile}'...")
|
||||
success = self._manager.ensure_running(self.config, self.profile)
|
||||
if not success:
|
||||
raise RuntimeError(f"Failed to start daemon for profile '{self.profile}'")
|
||||
raise RuntimeError(
|
||||
f"Failed to start daemon for profile '{self.profile}'"
|
||||
)
|
||||
|
||||
# Get daemon URL and create client
|
||||
daemon_url = self._manager.get_url(self.profile)
|
||||
@@ -375,3 +378,45 @@ class HindsightEmbedded:
|
||||
def is_running(self) -> bool:
|
||||
"""Check if the client is initialized."""
|
||||
return self._started and not self._closed and self._client is not None
|
||||
|
||||
def start_ui(self, ui_port: int | None = None, hostname: str = "0.0.0.0") -> bool:
|
||||
"""Start the control plane web UI.
|
||||
|
||||
The daemon is started automatically if not already running.
|
||||
|
||||
Args:
|
||||
ui_port: Port for the UI. Defaults to daemon_port + 10000.
|
||||
hostname: Hostname to bind to. Defaults to 0.0.0.0.
|
||||
|
||||
Returns:
|
||||
True if UI started successfully.
|
||||
"""
|
||||
self._ensure_started()
|
||||
return self._manager.start_ui(self.profile, ui_port, hostname)
|
||||
|
||||
def stop_ui(self, ui_port: int | None = None) -> bool:
|
||||
"""Stop the control plane web UI.
|
||||
|
||||
Args:
|
||||
ui_port: Port the UI is running on. Defaults to daemon_port + 10000.
|
||||
|
||||
Returns:
|
||||
True if stopped successfully.
|
||||
"""
|
||||
return self._manager.stop_ui(self.profile, ui_port)
|
||||
|
||||
def is_ui_running(self, ui_port: int | None = None) -> bool:
|
||||
"""Check if the control plane web UI is running.
|
||||
|
||||
Args:
|
||||
ui_port: Port to check. Defaults to daemon_port + 10000.
|
||||
|
||||
Returns:
|
||||
True if UI is running and responsive.
|
||||
"""
|
||||
return self._manager.is_ui_running(self.profile, ui_port)
|
||||
|
||||
@property
|
||||
def ui_url(self) -> str:
|
||||
"""Get the UI URL for this profile."""
|
||||
return self._manager.get_ui_url(self.profile)
|
||||
|
||||
@@ -683,6 +683,167 @@ def do_daemon(args, config: dict, logger):
|
||||
return 1
|
||||
|
||||
|
||||
def do_ui(args, config: dict, logger):
|
||||
"""Handle UI subcommands."""
|
||||
from . import daemon_client
|
||||
from .profile_manager import UI_PORT_OFFSET, ProfileManager
|
||||
|
||||
profile = args.profile
|
||||
ui_port = getattr(args, "port", None)
|
||||
hostname = getattr(args, "hostname", "0.0.0.0")
|
||||
|
||||
# Resolve default UI port
|
||||
pm = ProfileManager()
|
||||
paths = pm.resolve_profile_paths(profile or "")
|
||||
default_ui_port = paths.port + UI_PORT_OFFSET
|
||||
|
||||
if args.ui_command == "start":
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
console = Console()
|
||||
|
||||
effective_port = ui_port or default_ui_port
|
||||
|
||||
if daemon_client.is_ui_running(profile, effective_port):
|
||||
title = (
|
||||
f"[bold yellow]UI Already Running[/bold yellow] [dim]({profile or 'default'} @ :{effective_port})[/dim]"
|
||||
)
|
||||
console.print(
|
||||
Panel(
|
||||
Text("UI is already running", style="yellow"),
|
||||
title=title,
|
||||
border_style="yellow",
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
# Ensure daemon is running first
|
||||
if not daemon_client.is_daemon_running(profile):
|
||||
console.print("[dim]Daemon not running, starting it first...[/dim]")
|
||||
if not daemon_client.ensure_daemon_running(config, profile):
|
||||
console.print(
|
||||
Panel(
|
||||
Text("Failed to start daemon (required for UI)", style="red"),
|
||||
title="[bold red]✗ Error[/bold red]",
|
||||
border_style="red",
|
||||
)
|
||||
)
|
||||
return 1
|
||||
|
||||
if daemon_client.start_ui(profile, ui_port, hostname):
|
||||
return 0
|
||||
else:
|
||||
return 1
|
||||
|
||||
elif args.ui_command == "stop":
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
console = Console()
|
||||
effective_port = ui_port or default_ui_port
|
||||
|
||||
if not daemon_client.is_ui_running(profile, effective_port):
|
||||
title = f"[bold]UI Status[/bold] [dim]({profile or 'default'})[/dim]"
|
||||
console.print(
|
||||
Panel(
|
||||
Text("UI is not running", style="dim"),
|
||||
title=title,
|
||||
border_style="dim",
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
if daemon_client.stop_ui(profile, ui_port):
|
||||
title = f"[bold green]✓ UI Stopped[/bold green] [dim]({profile or 'default'})[/dim]"
|
||||
console.print(
|
||||
Panel(
|
||||
Text("UI stopped successfully", style="green"),
|
||||
title=title,
|
||||
border_style="green",
|
||||
)
|
||||
)
|
||||
return 0
|
||||
else:
|
||||
console.print(
|
||||
Panel(
|
||||
Text("Failed to stop UI", style="red"),
|
||||
title="[bold red]✗ Error[/bold red]",
|
||||
border_style="red",
|
||||
)
|
||||
)
|
||||
return 1
|
||||
|
||||
elif args.ui_command == "status":
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
console = Console()
|
||||
effective_port = ui_port or default_ui_port
|
||||
|
||||
if daemon_client.is_ui_running(profile, effective_port):
|
||||
status_text = Text()
|
||||
status_text.append("UI is running\n\n", style="green bold")
|
||||
status_text.append(" URL: ", style="dim")
|
||||
status_text.append(f"http://127.0.0.1:{effective_port}\n", style="cyan")
|
||||
status_text.append(" Logs: ", style="dim")
|
||||
status_text.append(f"{paths.ui_log}", style="")
|
||||
|
||||
title = f"[bold green]✓ UI Running[/bold green] [dim]({profile or 'default'} @ :{effective_port})[/dim]"
|
||||
console.print(
|
||||
Panel(
|
||||
status_text,
|
||||
title=title,
|
||||
border_style="green",
|
||||
padding=(1, 2),
|
||||
)
|
||||
)
|
||||
return 0
|
||||
else:
|
||||
title = f"[bold]UI Status[/bold] [dim]({profile or 'default'})[/dim]"
|
||||
console.print(
|
||||
Panel(
|
||||
Text("UI is not running", style="dim"),
|
||||
title=title,
|
||||
border_style="dim",
|
||||
)
|
||||
)
|
||||
return 1
|
||||
|
||||
elif args.ui_command == "logs":
|
||||
ui_log_path = paths.ui_log
|
||||
if not ui_log_path.exists():
|
||||
print("No UI logs found", file=sys.stderr)
|
||||
print(f" Expected at: {ui_log_path}")
|
||||
return 1
|
||||
|
||||
if args.follow:
|
||||
import subprocess
|
||||
|
||||
try:
|
||||
subprocess.run(["tail", "-f", str(ui_log_path)])
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
return 0
|
||||
else:
|
||||
try:
|
||||
with open(ui_log_path) as f:
|
||||
lines = f.readlines()
|
||||
for line in lines[-args.lines :]:
|
||||
print(line, end="")
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"Error reading logs: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
else:
|
||||
print("Usage: hindsight-embed ui {start|stop|status|logs}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
def _do_configure_profile_with_env(profile_name: str, port: int | None, env_vars: list[str]) -> int:
|
||||
"""Configure a named profile with environment variables (non-interactive).
|
||||
|
||||
@@ -1247,6 +1408,30 @@ def main():
|
||||
exit_code = do_daemon(args, config, logger)
|
||||
sys.exit(exit_code)
|
||||
|
||||
# Handle UI subcommands
|
||||
if command == "ui":
|
||||
parser = argparse.ArgumentParser(prog="hindsight-embed ui")
|
||||
subparsers = parser.add_subparsers(dest="ui_command")
|
||||
start_parser = subparsers.add_parser("start", help="Start the UI")
|
||||
start_parser.add_argument("--port", type=int, help="Port for the UI (default: daemon_port + 10000)")
|
||||
start_parser.add_argument(
|
||||
"--hostname", "-H", default="0.0.0.0", help="Hostname to bind to (default: 0.0.0.0)"
|
||||
)
|
||||
stop_parser = subparsers.add_parser("stop", help="Stop the UI")
|
||||
stop_parser.add_argument("--port", type=int, help="Port the UI is running on")
|
||||
status_parser = subparsers.add_parser("status", help="Check UI status")
|
||||
status_parser.add_argument("--port", type=int, help="Port to check")
|
||||
logs_parser = subparsers.add_parser("logs", help="View UI logs")
|
||||
logs_parser.add_argument("--follow", "-f", action="store_true")
|
||||
logs_parser.add_argument("--lines", "-n", type=int, default=50)
|
||||
|
||||
args = parser.parse_args(remaining_args[1:])
|
||||
args.profile = global_profile
|
||||
logger = setup_logging(False)
|
||||
config = get_config()
|
||||
exit_code = do_ui(args, config, logger)
|
||||
sys.exit(exit_code)
|
||||
|
||||
# Handle --help / -h
|
||||
if command in ("--help", "-h"):
|
||||
print_help()
|
||||
@@ -1305,6 +1490,12 @@ Daemon management:
|
||||
daemon status Check daemon status
|
||||
daemon logs [-f] [-n] View daemon logs
|
||||
|
||||
UI (control plane):
|
||||
ui start [--port PORT] [--hostname HOST] Start the web UI (default port: daemon_port + 10000)
|
||||
ui stop [--port PORT] Stop the web UI
|
||||
ui status [--port PORT] Check UI status
|
||||
ui logs [-f] [-n] View UI logs
|
||||
|
||||
CLI commands (forwarded to hindsight-cli):
|
||||
memory retain <bank> <content> Store a memory
|
||||
memory recall <bank> <query> Search memories
|
||||
|
||||
@@ -104,6 +104,67 @@ def is_daemon_running(profile: str | None = None) -> bool:
|
||||
return _manager.is_running(profile)
|
||||
|
||||
|
||||
def start_ui(profile: str | None = None, ui_port: int | None = None, hostname: str = "0.0.0.0") -> bool:
|
||||
"""Start the control plane UI.
|
||||
|
||||
Args:
|
||||
profile: Profile name (None = resolve from priority).
|
||||
ui_port: Port for the UI. Defaults to daemon_port + 10000.
|
||||
hostname: Hostname to bind to. Defaults to 0.0.0.0.
|
||||
|
||||
Returns:
|
||||
True if UI started successfully.
|
||||
"""
|
||||
if profile is None:
|
||||
profile = resolve_active_profile()
|
||||
return _manager.start_ui(profile, ui_port, hostname)
|
||||
|
||||
|
||||
def stop_ui(profile: str | None = None, ui_port: int | None = None) -> bool:
|
||||
"""Stop the control plane UI.
|
||||
|
||||
Args:
|
||||
profile: Profile name (None = resolve from priority).
|
||||
ui_port: Port the UI is running on. Defaults to daemon_port + 10000.
|
||||
|
||||
Returns:
|
||||
True if UI stopped successfully.
|
||||
"""
|
||||
if profile is None:
|
||||
profile = resolve_active_profile()
|
||||
return _manager.stop_ui(profile, ui_port)
|
||||
|
||||
|
||||
def is_ui_running(profile: str | None = None, ui_port: int | None = None) -> bool:
|
||||
"""Check if the UI is running.
|
||||
|
||||
Args:
|
||||
profile: Profile name (None = resolve from priority).
|
||||
ui_port: Port to check. Defaults to daemon_port + 10000.
|
||||
|
||||
Returns:
|
||||
True if UI is running and responsive.
|
||||
"""
|
||||
if profile is None:
|
||||
profile = resolve_active_profile()
|
||||
return _manager.is_ui_running(profile, ui_port)
|
||||
|
||||
|
||||
def get_ui_url(profile: str | None = None, ui_port: int | None = None) -> str:
|
||||
"""Get UI URL for a profile.
|
||||
|
||||
Args:
|
||||
profile: Profile name (None = resolve from priority).
|
||||
ui_port: Port for the UI. Defaults to daemon_port + 10000.
|
||||
|
||||
Returns:
|
||||
URL for the UI.
|
||||
"""
|
||||
if profile is None:
|
||||
profile = resolve_active_profile()
|
||||
return _manager.get_ui_url(profile, ui_port)
|
||||
|
||||
|
||||
def find_cli_binary() -> Path | None:
|
||||
"""Find the hindsight CLI binary in known locations or PATH."""
|
||||
import shutil
|
||||
|
||||
@@ -20,7 +20,7 @@ from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
from .embed_manager import EmbedManager
|
||||
from .profile_manager import ProfileManager, resolve_active_profile
|
||||
from .profile_manager import UI_PORT_OFFSET, ProfileManager, resolve_active_profile
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
console = Console(stderr=True)
|
||||
@@ -322,6 +322,219 @@ class DaemonEmbedManager(EmbedManager):
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to register profile '{profile}' in metadata: {e}")
|
||||
|
||||
def _find_ui_command(self) -> list[str]:
|
||||
"""Find the command to run the control plane UI."""
|
||||
# Check if we're in development mode (monorepo)
|
||||
dev_cp_path = Path(__file__).parent.parent.parent / "hindsight-control-plane"
|
||||
cli_js = dev_cp_path / "bin" / "cli.js"
|
||||
if cli_js.exists():
|
||||
return ["node", str(cli_js)]
|
||||
|
||||
# Use npx to run the published control plane package
|
||||
from . import __version__
|
||||
|
||||
cp_version = os.getenv("HINDSIGHT_EMBED_CP_VERSION", __version__)
|
||||
return ["npx", f"@vectorize-io/hindsight-control-plane@{cp_version}"]
|
||||
|
||||
def get_ui_url(self, profile: str, ui_port: int | None = None, hostname: str | None = None) -> str:
|
||||
"""Get the URL for the UI serving this profile."""
|
||||
if ui_port is None:
|
||||
paths = self._profile_manager.resolve_profile_paths(profile)
|
||||
ui_port = paths.port + UI_PORT_OFFSET
|
||||
host = hostname or "0.0.0.0"
|
||||
return f"http://{host}:{ui_port}"
|
||||
|
||||
def is_ui_running(self, profile: str, ui_port: int | None = None) -> bool:
|
||||
"""Check if the UI is running and responsive."""
|
||||
# Always health-check on 127.0.0.1 regardless of bind hostname
|
||||
ui_url = self.get_ui_url(profile, ui_port, hostname="127.0.0.1")
|
||||
try:
|
||||
with httpx.Client(timeout=2) as client:
|
||||
response = client.get(f"{ui_url}/api/health")
|
||||
return response.status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def start_ui(self, profile: str, ui_port: int | None = None, hostname: str = "0.0.0.0") -> bool:
|
||||
"""Start the control plane UI in background.
|
||||
|
||||
Args:
|
||||
profile: Profile name.
|
||||
ui_port: Port for the UI. Defaults to daemon_port + 10000.
|
||||
hostname: Hostname to bind to. Defaults to 0.0.0.0.
|
||||
|
||||
Returns:
|
||||
True if UI started successfully.
|
||||
"""
|
||||
paths = self._profile_manager.resolve_profile_paths(profile)
|
||||
if ui_port is None:
|
||||
ui_port = paths.port + UI_PORT_OFFSET
|
||||
|
||||
if self.is_ui_running(profile, ui_port):
|
||||
logger.debug(f"UI already running for profile '{profile}' on port {ui_port}")
|
||||
return True
|
||||
|
||||
profile_label = f"profile '{profile}'" if profile else "default profile"
|
||||
api_url = self.get_url(profile)
|
||||
ui_log = paths.ui_log
|
||||
|
||||
# Build environment
|
||||
env = os.environ.copy()
|
||||
env["PORT"] = str(ui_port)
|
||||
env["HOSTNAME"] = hostname
|
||||
env["HINDSIGHT_CP_DATAPLANE_API_URL"] = api_url
|
||||
|
||||
# Create log directory
|
||||
ui_log.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Build command
|
||||
cmd = self._find_ui_command() + [
|
||||
"--port",
|
||||
str(ui_port),
|
||||
"--hostname",
|
||||
hostname,
|
||||
"--api-url",
|
||||
api_url,
|
||||
]
|
||||
|
||||
try:
|
||||
log_file = open(ui_log, "w")
|
||||
subprocess.Popen(
|
||||
cmd,
|
||||
env=env,
|
||||
start_new_session=True,
|
||||
stdout=log_file,
|
||||
stderr=log_file,
|
||||
)
|
||||
|
||||
# Wait for UI to be ready
|
||||
start_time = time.time()
|
||||
title = f"[bold cyan]Starting UI[/bold cyan] [dim]({profile or 'default'} @ :{ui_port})[/dim]"
|
||||
log_lines = [f"Starting UI for {profile_label}...", ""]
|
||||
|
||||
with Live(console=console, auto_refresh=False) as live:
|
||||
content = Text("\n".join(log_lines), style="dim")
|
||||
panel = Panel(content, title=title, border_style="cyan", padding=(1, 2))
|
||||
live.update(panel)
|
||||
live.refresh()
|
||||
|
||||
while time.time() - start_time < 30:
|
||||
if self.is_ui_running(profile, ui_port):
|
||||
log_lines.append(f"✓ UI started at http://127.0.0.1:{ui_port}")
|
||||
log_lines.append(f"Logs: {ui_log}")
|
||||
content = Text("\n".join(log_lines), style="dim")
|
||||
success_title = (
|
||||
f"[bold green]✓ UI Started[/bold green] [dim]({profile or 'default'} @ :{ui_port})[/dim]"
|
||||
)
|
||||
panel = Panel(content, title=success_title, border_style="green", padding=(1, 2))
|
||||
live.update(panel)
|
||||
live.refresh()
|
||||
console.print()
|
||||
return True
|
||||
|
||||
elapsed = int(time.time() - start_time)
|
||||
status_msg = f"⏳ Waiting for UI... ({elapsed}s elapsed)"
|
||||
if log_lines and log_lines[-1].startswith("⏳"):
|
||||
log_lines[-1] = status_msg
|
||||
else:
|
||||
log_lines.append(status_msg)
|
||||
|
||||
content = Text("\n".join(log_lines), style="dim")
|
||||
panel = Panel(content, title=title, border_style="cyan", padding=(1, 2))
|
||||
live.update(panel)
|
||||
live.refresh()
|
||||
time.sleep(0.5)
|
||||
|
||||
# Timeout
|
||||
console.print(
|
||||
Panel(
|
||||
Text(f"UI failed to start (timeout)\n\nSee full log: {ui_log}", style="dim"),
|
||||
title=f"[bold red]✗ UI Failed (Timeout)[/bold red] [dim](:{ui_port})[/dim]",
|
||||
border_style="red",
|
||||
padding=(1, 2),
|
||||
)
|
||||
)
|
||||
console.print()
|
||||
return False
|
||||
|
||||
except FileNotFoundError:
|
||||
error_msg = (
|
||||
f"Command not found: {cmd[0]}\nFull command: {' '.join(cmd)}\n\nInstall Node.js and npx to run the UI."
|
||||
)
|
||||
console.print(
|
||||
Panel(
|
||||
Text(error_msg, style="red"),
|
||||
title="[bold red]✗ Command Not Found[/bold red]",
|
||||
border_style="red",
|
||||
padding=(1, 2),
|
||||
)
|
||||
)
|
||||
console.print()
|
||||
return False
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to start UI: {e}\n\nCommand: {' '.join(cmd)}\nLog file: {ui_log}"
|
||||
console.print(
|
||||
Panel(
|
||||
Text(error_msg, style="red"),
|
||||
title="[bold red]✗ UI Startup Error[/bold red]",
|
||||
border_style="red",
|
||||
padding=(1, 2),
|
||||
)
|
||||
)
|
||||
console.print()
|
||||
return False
|
||||
|
||||
def stop_ui(self, profile: str, ui_port: int | None = None) -> bool:
|
||||
"""Stop the UI for this profile.
|
||||
|
||||
Args:
|
||||
profile: Profile name.
|
||||
ui_port: Port the UI is running on. Defaults to daemon_port + 10000.
|
||||
|
||||
Returns:
|
||||
True if stopped successfully.
|
||||
"""
|
||||
paths = self._profile_manager.resolve_profile_paths(profile)
|
||||
if ui_port is None:
|
||||
ui_port = paths.port + UI_PORT_OFFSET
|
||||
|
||||
if not self.is_ui_running(profile, ui_port):
|
||||
logger.debug(f"UI not running for profile '{profile}'")
|
||||
return True
|
||||
|
||||
# Find PID by port
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["lsof", "-ti", f":{ui_port}", "-sTCP:LISTEN"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
pid = int(result.stdout.strip().split()[0])
|
||||
logger.debug(f"Found UI PID {pid} on port {ui_port}")
|
||||
os.kill(pid, 15)
|
||||
|
||||
# Wait for process to exit
|
||||
for _ in range(50):
|
||||
time.sleep(0.1)
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
except OSError:
|
||||
break
|
||||
else:
|
||||
logger.warning(f"Could not find PID for UI port {ui_port}")
|
||||
except (subprocess.TimeoutExpired, ValueError, OSError, FileNotFoundError) as e:
|
||||
logger.warning(f"Could not find/kill UI by port: {e}")
|
||||
|
||||
# Wait for health check to fail
|
||||
for _ in range(30):
|
||||
if not self.is_ui_running(profile, ui_port):
|
||||
return True
|
||||
time.sleep(0.1)
|
||||
|
||||
return not self.is_ui_running(profile, ui_port)
|
||||
|
||||
def ensure_running(self, config: dict, profile: str) -> bool:
|
||||
"""
|
||||
Ensure daemon is running, starting it if needed.
|
||||
|
||||
@@ -28,6 +28,10 @@ PROFILE_PORT_BASE = 8889
|
||||
PROFILE_PORT_RANGE = 1000 # 8889-9888
|
||||
|
||||
|
||||
# UI port offset from daemon port (e.g., daemon 8888 -> UI 18888)
|
||||
UI_PORT_OFFSET = 10000
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProfilePaths:
|
||||
"""Paths and port for a profile."""
|
||||
@@ -36,6 +40,11 @@ class ProfilePaths:
|
||||
lock: Path
|
||||
log: Path
|
||||
port: int
|
||||
ui_log: Path = None # type: ignore[assignment]
|
||||
|
||||
def __post_init__(self):
|
||||
if self.ui_log is None:
|
||||
self.ui_log = self.log.parent / self.log.name.replace(".log", ".ui.log")
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
Reference in New Issue
Block a user