This commit is contained in:
2026-01-22 00:22:42 -08:00
parent 51593536af
commit 81f29c7025
3 changed files with 234 additions and 85 deletions

2
run-client.bat Normal file
View File

@@ -0,0 +1,2 @@
@echo off
"C:\hydrusnetwork\.venv\Scripts\python.exe" "C:\Forgejo\Medios-Macina\scripts\run_client.py" %*

View File

@@ -83,26 +83,38 @@ def _ensure_interactive_stdin() -> None:
print(f"DEBUG: Failed to re-open stdin: {e}") print(f"DEBUG: Failed to re-open stdin: {e}")
def run(cmd: list[str], quiet: bool = False, debug: bool = False, cwd: Optional[Path] = None, env: Optional[dict[str, str]] = None) -> None: def run(cmd: list[str], quiet: bool = False, debug: bool = False, cwd: Optional[Path] = None, env: Optional[dict[str, str]] = None, check: bool = True) -> subprocess.CompletedProcess:
if debug: if debug:
print(f"\n> {' '.join(cmd)}") print(f"\n> {' '.join(cmd)}")
# Create a copy of the environment to potentially modify it for pip
run_env = env.copy() if env is not None else os.environ.copy()
# If we are running a python command, ensure we don't leak user site-packages
# which can cause "ModuleNotFoundError: No module named 'attr'" errors in recent pip/rich,
# and ensures we only use packages installed in our venv.
if len(cmd) >= 1 and "python" in str(cmd[0]).lower():
run_env["PYTHONNOUSERSITE"] = "1"
# Also clear any other potentially conflicting variables
run_env.pop("PYTHONPATH", None)
# Ensure subprocess uses the re-opened interactive stdin if we have one # Ensure subprocess uses the re-opened interactive stdin if we have one
stdin_handle = sys.stdin if not sys.stdin.isatty() or platform.system().lower() == "windows" else None stdin_handle = sys.stdin if not sys.stdin.isatty() or platform.system().lower() == "windows" else None
if quiet and not debug: if quiet and not debug:
subprocess.check_call( return subprocess.run(
cmd, cmd,
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
cwd=str(cwd) if cwd else None, cwd=str(cwd) if cwd else None,
env=env, env=run_env,
stdin=stdin_handle stdin=stdin_handle,
check=check
) )
else: else:
if not debug: if not debug:
print(f"> {' '.join(cmd)}") print(f"> {' '.join(cmd)}")
subprocess.check_call(cmd, cwd=str(cwd) if cwd else None, env=env, stdin=stdin_handle) return subprocess.run(cmd, cwd=str(cwd) if cwd else None, env=run_env, stdin=stdin_handle, check=check)
REPO_URL = "https://code.glowers.club/goyimnose/Medios-Macina.git" REPO_URL = "https://code.glowers.club/goyimnose/Medios-Macina.git"
@@ -121,6 +133,7 @@ class ProgressBar:
if self.quiet: if self.quiet:
return return
term_width = shutil.get_terminal_size((80, 20)).columns term_width = shutil.get_terminal_size((80, 20)).columns
percent = int(100 * (self.current / self.total)) percent = int(100 * (self.current / self.total))
filled = int(self.bar_width * self.current // self.total) filled = int(self.bar_width * self.current // self.total)
@@ -513,28 +526,33 @@ def main() -> int:
# Running via pipe/eval, __file__ is not defined # Running via pipe/eval, __file__ is not defined
script_path = None script_path = None
script_dir = Path.cwd() script_dir = Path.cwd()
repo_root = Path.cwd() # In Web Installer mode, we don't assume CWD is the repo root.
repo_root = None
# DETECT REPOSITORY # DETECT REPOSITORY
# Check if we are already inside a valid Medios-Macina repo # Check if we are already inside a valid Medios-Macina repo
def _is_valid_mm_repo(p: Path) -> bool: def _is_valid_mm_repo(p: Path | None) -> bool:
if p is None: return False
return (p / "CLI.py").exists() and (p / "scripts").exists() return (p / "CLI.py").exists() and (p / "scripts").exists()
# If running from a pipe/standalone, don't auto-probe the current directory # If running from a pipe/standalone, don't auto-probe the current directory
# as a repo. This prevents "auto-choosing" existing folders in Web Installer mode. # as a repo. This prevents "auto-choosing" existing folders in Web Installer mode.
is_in_repo = False
if script_path is None: if script_path is None:
is_in_repo = False pass
else: else:
is_in_repo = _is_valid_mm_repo(repo_root) # Check if we are already inside a valid Medios-Macina repo.
# If not in the parent of the script, check the current working directory # We do this to provide a sensible default if we need to prompt the user.
if not is_in_repo and _is_valid_mm_repo(Path.cwd()): # But we don't set is_in_repo = True yet, so that _ensure_repo_available
repo_root = Path.cwd() # will still prompt for confirmation in interactive mode.
script_dir = repo_root / "scripts" if not _is_valid_mm_repo(repo_root):
is_in_repo = True if _is_valid_mm_repo(Path.cwd()):
repo_root = Path.cwd()
script_dir = repo_root / "scripts"
if not args.quiet: if not args.quiet and args.debug:
print(f"Bootstrap script location: {script_dir}") print(f"Bootstrap script location: {script_dir}")
print(f"Detected project root: {repo_root}") print(f"Project root: {repo_root}")
print(f"Current working directory: {Path.cwd()}") print(f"Current working directory: {Path.cwd()}")
# Helpers for interactive menu and uninstall detection # Helpers for interactive menu and uninstall detection
@@ -548,13 +566,18 @@ def main() -> int:
def _is_installed() -> bool: def _is_installed() -> bool:
"""Return True if the project appears installed into the local .venv.""" """Return True if the project appears installed into the local .venv."""
if repo_root is None:
return False
vdir = repo_root / ".venv" vdir = repo_root / ".venv"
py = _venv_python_path(vdir) py = _venv_python_path(vdir)
if py is None: if py is None:
return False return False
try: try:
rc = subprocess.run([str(py), "-m", "pip", "show", "medeia-macina"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False) # We use the global run() with check=False to avoid raising CalledProcessError
return rc.returncode == 0 # when the package is not found (which returns exit code 1).
res = run([str(py), "-m", "pip", "show", "medeia-macina"], quiet=True, check=False)
return res.returncode == 0
except Exception: except Exception:
return False return False
@@ -745,15 +768,21 @@ def main() -> int:
"""Show a simple interactive menu to choose install/uninstall or delegate.""" """Show a simple interactive menu to choose install/uninstall or delegate."""
try: try:
installed = _is_installed() installed = _is_installed()
term_width = shutil.get_terminal_size((80, 20)).columns
while True: while True:
os.system("cls" if os.name == "nt" else "clear") os.system("cls" if os.name == "nt" else "clear")
# Center the logo term_width = shutil.get_terminal_size((80, 20)).columns
logo_lines = LOGO.strip().splitlines()
# Use the same centering logic as the main installation screen
logo_lines = LOGO.strip('\n').splitlines()
max_logo_width = 0
for line in logo_lines:
max_logo_width = max(max_logo_width, len(line.rstrip()))
logo_padding = ' ' * max((term_width - max_logo_width) // 2, 0)
print("\n" * 2) print("\n" * 2)
for line in logo_lines: for line in logo_lines:
print(line.center(term_width)) print(f"{logo_padding}{line.rstrip()}")
print("\n") print("\n")
menu_title = " MEDEIA MACINA BOOTSTRAP MENU " menu_title = " MEDEIA MACINA BOOTSTRAP MENU "
@@ -763,17 +792,29 @@ def main() -> int:
print(border.center(term_width)) print(border.center(term_width))
print("\n") print("\n")
install_text = "1) Reinstall" if installed else "1) Install" # Define menu options
print(install_text.center(term_width)) options = [
print("2) Extras > HydrusNetwork".center(term_width)) "1) Reinstall" if installed else "1) Install",
"2) Extras > HydrusNetwork"
]
if installed: if installed:
print("3) Uninstall".center(term_width)) options.append("3) Uninstall")
print("q) Quit".center(term_width)) options.append("4) Install Hydrus System Service (Auto-update + Headless)")
options.append("q) Quit")
# Center the block of options by finding the longest one
max_opt_width = max(len(opt) for opt in options)
opt_padding = ' ' * max((term_width - max_opt_width) // 2, 0)
for opt in options:
print(f"{opt_padding}{opt}")
prompt = "\nChoose an option: " prompt = "\nChoose an option: "
# Try to center the prompt roughly # Try to center the prompt roughly
indent = " " * ((term_width // 2) - (len(prompt) // 2)) indent = " " * max((term_width // 2) - (len(prompt) // 2), 0)
choice = input(f"{indent}{prompt}").strip().lower() sys.stdout.write(f"{indent}{prompt}")
sys.stdout.flush()
choice = sys.stdin.readline().strip().lower()
if choice in ("1", "install", "reinstall"): if choice in ("1", "install", "reinstall"):
return "install" return "install"
@@ -784,6 +825,9 @@ def main() -> int:
if installed and choice in ("3", "uninstall"): if installed and choice in ("3", "uninstall"):
return "uninstall" return "uninstall"
if choice == "4":
return "install_service"
if choice in ("q", "quit", "exit"): if choice in ("q", "quit", "exit"):
return 0 return 0
except EOFError: except EOFError:
@@ -794,16 +838,10 @@ def main() -> int:
"""Prompt for a clone location when running outside the repository.""" """Prompt for a clone location when running outside the repository."""
nonlocal repo_root, script_dir, is_in_repo nonlocal repo_root, script_dir, is_in_repo
# If script_path is None, we are running from a pipe/URL. # If we have already settled on a repository path in this session, skip.
# In this mode, we ALWAYS want to ask for the directory, if is_in_repo and repo_root is not None:
# unless we have already asked and set is_in_repo to True within this session.
if is_in_repo and script_path is not None:
return True return True
# If we are in standalone mode and is_in_repo is True, it means we
# just finished cloning/setting up the path in the current execution.
# But we need to be careful: if we haven't asked yet, we must ask.
if not shutil.which("git"): if not shutil.which("git"):
print("\nError: 'git' was not found on your PATH.", file=sys.stderr) print("\nError: 'git' was not found on your PATH.", file=sys.stderr)
print("Please install Git (https://git-scm.com/) and try again.", file=sys.stderr) print("Please install Git (https://git-scm.com/) and try again.", file=sys.stderr)
@@ -852,6 +890,7 @@ def main() -> int:
return False return False
os.chdir(str(repo_root)) os.chdir(str(repo_root))
is_in_repo = True
if not args.quiet: if not args.quiet:
print(f"\nSuccessfully set up repository at {repo_root}") print(f"\nSuccessfully set up repository at {repo_root}")
print("Resuming bootstrap...\n") print("Resuming bootstrap...\n")
@@ -1012,18 +1051,12 @@ def main() -> int:
if (sys.stdin.isatty() or sys.stdout.isatty() or script_path is None) and not args.quiet: if (sys.stdin.isatty() or sys.stdout.isatty() or script_path is None) and not args.quiet:
sel = _interactive_menu() sel = _interactive_menu()
if sel == "install": if sel == "install":
# Force set is_in_repo to False if piped to ensure _ensure_repo_available asks
if script_path is None:
is_in_repo = False
if not _ensure_repo_available(): if not _ensure_repo_available():
return 1 return 1
args.skip_deps = False args.skip_deps = False
args.install_editable = True args.install_editable = True
args.no_playwright = False args.no_playwright = False
elif sel == "extras_hydrus": elif sel == "extras_hydrus":
# Force set is_in_repo to False if piped to ensure _ensure_repo_available asks
if script_path is None:
is_in_repo = False
if not _ensure_repo_available(): if not _ensure_repo_available():
return 1 return 1
hydrus_script = repo_root / "scripts" / "hydrusnetwork.py" hydrus_script = repo_root / "scripts" / "hydrusnetwork.py"
@@ -1049,6 +1082,57 @@ def main() -> int:
else: else:
print(f"\nError: {hydrus_script} not found.") print(f"\nError: {hydrus_script} not found.")
return 0 return 0
elif sel == "install_service":
# Direct path input for the target repository
print("\n[ SYSTEM SERVICE INSTALLATION ]")
print("Enter the root directory of the Hydrus repository you want to run as a service.")
print("This is the folder containing 'hydrus_client.py'.")
# Default to repo_root/hydrusnetwork if available, otherwise CWD
default_path = repo_root / "hydrusnetwork" if repo_root else Path.cwd()
sys.stdout.write(f"Repository Root [{default_path}]: ")
sys.stdout.flush()
path_raw = sys.stdin.readline().strip()
target_repo = Path(path_raw).resolve() if path_raw else default_path
if not (target_repo / "hydrus_client.py").exists():
print(f"\n[!] Error: 'hydrus_client.py' not found in: {target_repo}")
print(" Please ensure you've entered the correct repository root.")
sys.stdout.write("\nPress Enter to return to menu...")
sys.stdout.flush()
sys.stdin.readline()
return "menu"
run_client_script = repo_root / "scripts" / "run_client.py" if repo_root else Path(__file__).parent / "run_client.py"
if run_client_script.exists():
try:
# We pass --repo-root explicitly to the target_repo provided by the user
subprocess.check_call(
[
sys.executable,
str(run_client_script),
"--install-service",
"--service-name", "hydrus-client",
"--repo-root", str(target_repo),
"--headless",
"--pull"
],
stdin=sys.stdin
)
print("\nHydrus System service installed successfully.")
except subprocess.CalledProcessError:
print("\nService installation failed.")
except Exception as e:
print(f"\nError installing service: {e}")
else:
print(f"\nError: {run_client_script} not found.")
sys.stdout.write("\nPress Enter to continue...")
sys.stdout.flush()
sys.stdin.readline()
return "menu"
elif sel == "uninstall": elif sel == "uninstall":
return _do_uninstall() return _do_uninstall()
elif sel == "delegate": elif sel == "delegate":
@@ -1087,6 +1171,10 @@ def main() -> int:
print(f"{padding}{line.rstrip()}") print(f"{padding}{line.rstrip()}")
print("\n") print("\n")
if repo_root is None:
print("Error: No project repository found. Please ensure you are running this script inside the project folder or follow the interactive install prompts.", file=sys.stderr)
return 1
# Determine total steps for progress bar # Determine total steps for progress bar
total_steps = 7 # Base: venv, pip, deps, project, cli, finalize, env total_steps = 7 # Base: venv, pip, deps, project, cli, finalize, env
if args.upgrade_pip: total_steps += 1 if args.upgrade_pip: total_steps += 1
@@ -1139,19 +1227,17 @@ def main() -> int:
"""Ensure pip is available inside the venv; fall back to ensurepip if needed.""" """Ensure pip is available inside the venv; fall back to ensurepip if needed."""
try: try:
subprocess.run( # Use run() to ensure clean environment (fixes ModuleNotFoundError: No module named 'attr' in pipe mode)
[str(python_path), "-m", "pip", "--version"], run([str(python_path), "-m", "pip", "--version"], quiet=True)
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=True,
)
return return
except Exception: except Exception:
pass pass
try: try:
_run_cmd([str(python_path), "-m", "ensurepip", "--upgrade"]) # ensurepip is a stdlib module, but it can still benefit from a clean environment
except subprocess.CalledProcessError: # although usually it doesn't use site-packages.
run([str(python_path), "-m", "ensurepip", "--upgrade"], quiet=True)
except Exception:
print( print(
"Failed to install pip inside the local virtualenv via ensurepip; ensure your Python build includes ensurepip and retry.", "Failed to install pip inside the local virtualenv via ensurepip; ensure your Python build includes ensurepip and retry.",
file=sys.stderr, file=sys.stderr,
@@ -1239,15 +1325,12 @@ def main() -> int:
pb.update("Verifying CLI configuration...") pb.update("Verifying CLI configuration...")
# Check core dependencies (including mpv) before finalizing # Check core dependencies (including mpv) before finalizing
try: try:
# Check basic imports and specifically mpv
missing = [] missing = []
for mod in ["importlib", "shutil", "subprocess", "mpv"]: for mod in ["importlib", "shutil", "subprocess", "mpv"]:
try: try:
subprocess.run( # Use run() for verification to ensure clean environment
[str(venv_python), "-c", f"import {mod}"], run([str(venv_python), "-c", f"import {mod}"], quiet=True)
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=True
)
except Exception: except Exception:
missing.append(mod) missing.append(mod)
@@ -1267,17 +1350,11 @@ def main() -> int:
pass pass
try: try:
cli_verify_result = subprocess.run( # Use run() for CLI verification to ensure clean environment (fixes global interference)
[ run([str(venv_python), "-c", "import importlib; importlib.import_module('CLI')"], quiet=True)
str(venv_python), except Exception:
"-c", try:
"import importlib; importlib.import_module('CLI')" # If direct import fails, try to diagnose why or add a .pth link
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False,
)
if cli_verify_result.returncode != 0:
cmd = [ cmd = [
str(venv_python), str(venv_python),
"-c", "-c",
@@ -1291,7 +1368,7 @@ def main() -> int:
"for s in res:\n print(s)\n" "for s in res:\n print(s)\n"
), ),
] ]
out = subprocess.check_output(cmd, text=True).strip().splitlines() out = subprocess.check_output(cmd, text=True, env={**os.environ, "PYTHONNOUSERSITE": "1"}).strip().splitlines()
site_dir: Path | None = None site_dir: Path | None = None
for sp in out: for sp in out:
if sp and Path(sp).exists(): if sp and Path(sp).exists():
@@ -1308,6 +1385,8 @@ def main() -> int:
else: else:
with pth_file.open("w", encoding="utf-8") as fh: with pth_file.open("w", encoding="utf-8") as fh:
fh.write(content) fh.write(content)
except Exception:
pass
except Exception: except Exception:
pass pass

View File

@@ -217,6 +217,8 @@ def install_service_windows(
headless: bool = True, headless: bool = True,
detached: bool = True, detached: bool = True,
start_on: str = "logon", start_on: str = "logon",
pull: bool = False,
workspace_root: Optional[Path] = None,
) -> bool: ) -> bool:
try: try:
schtasks = shutil.which("schtasks") schtasks = shutil.which("schtasks")
@@ -226,14 +228,27 @@ def install_service_windows(
) )
return False return False
bat = repo_root / "run-client.bat" # If we have a workspace root (Medios-Macina), we use it for the wrapper scripts
base_dir = workspace_root if workspace_root else repo_root
bat = base_dir / "run-client.bat"
python_exe = venv_py
this_script = Path(__file__).resolve()
if not bat.exists(): if not bat.exists():
# Use escaped backslashes to avoid Python "invalid escape sequence" warnings # Absolute paths ensure the service works regardless of where it starts
content = '@echo off\n"%~dp0\\.venv\\Scripts\\python.exe" "%~dp0hydrus_client.py" %*\n' content = f'@echo off\n"{python_exe}" "{this_script}" %*\n'
bat.write_text(content, encoding="utf-8") bat.write_text(content, encoding="utf-8")
tr = str(bat) tr = str(bat)
sc = "ONLOGON" if start_on == "logon" else "ONSTART" sc = "ONLOGON" if start_on == "logon" else "ONSTART"
task_args = "--detached "
if headless: task_args += "--headless "
if pull: task_args += "--pull "
# Force the correct repo root for the service
task_args += f'--repo-root "{repo_root}" '
cmd = [ cmd = [
schtasks, schtasks,
"/Create", "/Create",
@@ -242,7 +257,7 @@ def install_service_windows(
"/TN", "/TN",
service_name, service_name,
"/TR", "/TR",
f'"{tr}"', f'{tr} {task_args}',
"/RL", "/RL",
"LIMITED", "LIMITED",
"/F", "/F",
@@ -283,7 +298,9 @@ def install_service_systemd(
repo_root: Path, repo_root: Path,
venv_py: Path, venv_py: Path,
headless: bool = True, headless: bool = True,
detached: bool = True detached: bool = True,
pull: bool = False,
workspace_root: Optional[Path] = None,
) -> bool: ) -> bool:
try: try:
systemctl = shutil.which("systemctl") systemctl = shutil.which("systemctl")
@@ -296,15 +313,22 @@ def install_service_systemd(
repo_root, repo_root,
venv_py, venv_py,
headless, headless,
detached detached,
pull=pull,
workspace_root=workspace_root
) )
unit_dir = Path.home() / ".config" / "systemd" / "user" unit_dir = Path.home() / ".config" / "systemd" / "user"
unit_dir.mkdir(parents=True, exist_ok=True) unit_dir.mkdir(parents=True, exist_ok=True)
unit_file = unit_dir / f"{service_name}.service" unit_file = unit_dir / f"{service_name}.service"
exec_args = f'"{venv_py}" "{str(repo_root / "run_client.py")}" --detached '
exec_args += "--headless " if headless else "--gui " this_script = Path(__file__).resolve()
content = f"[Unit]\nDescription=Hydrus Client (user)\nAfter=network.target\n\n[Service]\nType=simple\nExecStart={exec_args}\nWorkingDirectory={str(repo_root)}\nRestart=on-failure\nEnvironment=PYTHONUNBUFFERED=1\n\n[Install]\nWantedBy=default.target\n" exec_args = f'"{venv_py}" "{this_script}" --detached '
if headless: exec_args += "--headless "
if pull: exec_args += "--pull "
exec_args += f'--repo-root "{repo_root}" '
content = f"[Unit]\nDescription=Medios-Macina Client\nAfter=network.target\n\n[Service]\nType=simple\nExecStart={exec_args}\nWorkingDirectory={str(workspace_root if workspace_root else repo_root)}\nRestart=on-failure\nEnvironment=PYTHONUNBUFFERED=1\n\n[Install]\nWantedBy=default.target\n"
unit_file.write_text(content, encoding="utf-8") unit_file.write_text(content, encoding="utf-8")
subprocess.run([systemctl, "--user", "daemon-reload"], check=True) subprocess.run([systemctl, "--user", "daemon-reload"], check=True)
subprocess.run( subprocess.run(
@@ -356,14 +380,23 @@ def install_service_cron(
repo_root: Path, repo_root: Path,
venv_py: Path, venv_py: Path,
headless: bool = True, headless: bool = True,
detached: bool = True detached: bool = True,
pull: bool = False,
workspace_root: Optional[Path] = None,
) -> bool: ) -> bool:
try: try:
crontab = shutil.which("crontab") crontab = shutil.which("crontab")
if not crontab: if not crontab:
print("crontab not available; cannot install reboot cron job.") print("crontab not available; cannot install reboot cron job.")
return False return False
entry = f"@reboot {venv_py} {str(repo_root / 'run_client.py')} --detached {'--headless' if headless else '--gui'} # {service_name}\n"
this_script = Path(__file__).resolve()
exec_args = f'"{venv_py}" "{this_script}" --detached '
if headless: exec_args += "--headless "
if pull: exec_args += "--pull "
exec_args += f'--repo-root "{repo_root}" '
entry = f"@reboot {exec_args} # {service_name}\n"
proc = subprocess.run( proc = subprocess.run(
[crontab, [crontab,
"-l"], "-l"],
@@ -421,7 +454,9 @@ def install_service_auto(
repo_root: Path, repo_root: Path,
venv_py: Path, venv_py: Path,
headless: bool = True, headless: bool = True,
detached: bool = True detached: bool = True,
pull: bool = False,
workspace_root: Optional[Path] = None,
) -> bool: ) -> bool:
try: try:
if os.name == "nt": if os.name == "nt":
@@ -430,7 +465,9 @@ def install_service_auto(
repo_root, repo_root,
venv_py, venv_py,
headless=headless, headless=headless,
detached=detached detached=detached,
pull=pull,
workspace_root=workspace_root
) )
else: else:
if shutil.which("systemctl"): if shutil.which("systemctl"):
@@ -439,7 +476,9 @@ def install_service_auto(
repo_root, repo_root,
venv_py, venv_py,
headless=headless, headless=headless,
detached=detached detached=detached,
pull=pull,
workspace_root=workspace_root
) )
else: else:
return install_service_cron( return install_service_cron(
@@ -447,11 +486,16 @@ def install_service_auto(
repo_root, repo_root,
venv_py, venv_py,
headless=headless, headless=headless,
detached=detached detached=detached,
pull=pull,
workspace_root=workspace_root
) )
except Exception as exc: except Exception as exc:
print("install_service_auto error:", exc) print("install_service_auto error:", exc)
return False return False
except Exception as exc:
print("install_service_auto error:", exc)
return False
def uninstall_service_auto(service_name: str, repo_root: Path, venv_py: Path) -> bool: def uninstall_service_auto(service_name: str, repo_root: Path, venv_py: Path) -> bool:
@@ -602,6 +646,11 @@ def main(argv: Optional[List[str]] = None) -> int:
help= help=
"Attempt to launch the client without showing the Qt GUI (best-effort). Default for subsequent runs; first run will show GUI unless --headless is supplied", "Attempt to launch the client without showing the Qt GUI (best-effort). Default for subsequent runs; first run will show GUI unless --headless is supplied",
) )
p.add_argument(
"--pull",
action="store_true",
help="Run 'git pull' before starting the client",
)
p.add_argument( p.add_argument(
"--gui", "--gui",
action="store_true", action="store_true",
@@ -660,6 +709,23 @@ def main(argv: Optional[List[str]] = None) -> int:
else: else:
repo_root = workspace_root repo_root = workspace_root
# Handle git pull update if requested
if args.pull:
if shutil.which("git"):
if (repo_root / ".git").exists():
if not args.quiet:
print(f"Updating repository via 'git pull' in {repo_root}...")
try:
subprocess.run(["git", "pull"], cwd=str(repo_root), check=False)
except Exception as e:
print(f"Warning: git pull failed: {e}")
else:
if not args.quiet:
print("Skipping 'git pull': directory is not a git repository.")
else:
if not args.quiet:
print("Skipping 'git pull': 'git' not found on PATH.")
venv_py = find_venv_python(repo_root, args.venv, args.venv_name) venv_py = find_venv_python(repo_root, args.venv, args.venv_name)
def _is_running_in_virtualenv() -> bool: def _is_running_in_virtualenv() -> bool:
@@ -817,7 +883,9 @@ def main(argv: Optional[List[str]] = None) -> int:
repo_root, repo_root,
venv_py, venv_py,
headless=use_headless, headless=use_headless,
detached=True detached=True,
pull=args.pull,
workspace_root=workspace_root
) )
return 0 if ok else 6 return 0 if ok else 6
if args.uninstall_service: if args.uninstall_service: