2025-12-25 05:10:39 -08:00
#!/usr/bin/env python3
"""scripts/bootstrap.py
Unified project bootstrap helper (Python-only).
2025-12-31 22:05:25 -08:00
This script installs Python dependencies from `scripts/requirements.txt` and then
2025-12-25 05:10:39 -08:00
downloads Playwright browser binaries by running `python -m playwright install`.
By default this script installs **Chromium** only to conserve space; pass
`--browsers all` to install all supported engines (chromium, firefox, webkit).
2026-01-09 16:02:49 -08:00
FFmpeg: The project includes ffmpeg binaries for Windows (in MPV/ffmpeg). On Linux/macOS,
install ffmpeg using your system package manager (apt install ffmpeg, brew install ffmpeg, etc.).
ffmpeg-python is installed as a dependency, but requires ffmpeg itself to be on your PATH.
2025-12-31 16:10:35 -08:00
Note: This Python script is the canonical installer for the project — prefer
running `python ./scripts/bootstrap.py` locally. The platform scripts
2026-04-01 13:58:06 -07:00
(`scripts/bootstrap.ps1` and `scripts/bootstrap.sh`) are thin wrappers
that delegate to this script.
2025-12-31 16:10:35 -08:00
2026-04-01 13:58:06 -07:00
The install flow is owned here so `bootstrap.py` remains the single global
entry point, while platform wrappers only provide shell-specific convenience.
2025-12-25 05:10:39 -08:00
This file replaces the old `scripts/setup.py` to ensure the repository only has
one `setup.py` (at the repository root) for packaging.
Usage:
2026-04-01 13:58:06 -07:00
python ./scripts/bootstrap.py # install deps and playwright browsers
2025-12-25 05:10:39 -08:00
python ./scripts/bootstrap.py --skip-deps
python ./scripts/bootstrap.py --playwright-only
Optional flags:
2025-12-31 22:05:25 -08:00
--skip-deps Skip `pip install -r scripts/requirements.txt` step
2025-12-25 05:10:39 -08:00
--no-playwright Skip running `python -m playwright install` (still installs deps)
--playwright-only Install only Playwright browsers (installs playwright package if missing)
--browsers Comma-separated list of Playwright browsers to install (default: chromium)
2026-01-09 16:57:27 -08:00
--install-editable Install the project in editable mode (pip install -e scripts) for running tests
2026-01-10 17:30:18 -08:00
--install-mpv Install MPV player if not already installed (default)
--no-mpv Skip installing MPV player
--install-deno Install the Deno runtime using the official installer (default)
2025-12-25 05:10:39 -08:00
--no-deno Skip installing the Deno runtime
--deno-version Pin a specific Deno version to install (e.g., v1.34.3)
--upgrade-pip Upgrade pip, setuptools, and wheel before installing deps
2026-01-09 16:36:56 -08:00
--check-install Verify that the 'mm' command was installed correctly
--debug Show detailed diagnostic information during installation
--quiet Suppress output (used internally by platform scripts)
2025-12-25 05:10:39 -08:00
"""
from __future__ import annotations
import argparse
import os
import platform
2026-01-19 06:24:09 -08:00
import re
2026-01-22 03:52:07 -08:00
import tempfile
2026-01-22 03:47:01 -08:00
import urllib.request
2025-12-25 05:10:39 -08:00
from pathlib import Path
import shutil
import subprocess
import sys
import time
2026-01-19 06:24:09 -08:00
from typing import Optional
2025-12-25 05:10:39 -08:00
2026-01-21 23:01:18 -08:00
def _ensure_interactive_stdin () -> None :
"""If stdin is piped (e.g. via curl | python), re-open it to the terminal."""
if not sys . stdin . isatty ():
try :
if platform . system () . lower () == "windows" :
2026-01-21 23:27:48 -08:00
# Ensure the handle is actually opened for reading correctly
new_stdin = open ( "CONIN$" , "r" )
sys . stdin = new_stdin
2026-01-21 23:01:18 -08:00
else :
sys . stdin = open ( "/dev/tty" , "r" )
2026-01-21 23:27:48 -08:00
# Flush existing buffers to ensure clean state
if hasattr ( sys . stdin , 'flush' ):
sys . stdin . flush ()
except Exception as e :
if "--debug" in sys . argv :
2026-04-26 15:08:35 -07:00
print ( f "[bootstrap] Failed to re-open stdin: { e } " )
2026-01-21 23:01:18 -08:00
2026-01-22 00:22:42 -08:00
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 :
2026-01-12 13:51:26 -08:00
if debug :
print ( f " \n > { ' ' . join ( cmd ) } " )
2026-01-22 00:22:42 -08:00
# 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 )
2026-01-21 23:30:19 -08:00
# 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
2026-01-12 13:51:26 -08:00
if quiet and not debug :
2026-01-22 00:22:42 -08:00
return subprocess . run (
2026-01-12 13:51:26 -08:00
cmd ,
stdout = subprocess . DEVNULL ,
stderr = subprocess . DEVNULL ,
2026-01-21 23:27:48 -08:00
cwd = str ( cwd ) if cwd else None ,
2026-01-22 00:22:42 -08:00
env = run_env ,
stdin = stdin_handle ,
check = check
2026-01-12 13:51:26 -08:00
)
else :
if not debug :
print ( f "> { ' ' . join ( cmd ) } " )
2026-01-22 00:22:42 -08:00
return subprocess . run ( cmd , cwd = str ( cwd ) if cwd else None , env = run_env , stdin = stdin_handle , check = check )
2026-01-12 13:51:26 -08:00
2026-01-21 22:52:52 -08:00
REPO_URL = "https://code.glowers.club/goyimnose/Medios-Macina.git"
2026-01-22 03:52:07 -08:00
HYDRUS_REPO_URL = "https://github.com/hydrusnetwork/hydrus.git"
2026-04-29 17:15:56 -07:00
HYDRUS_WEB_GUI_REPO_URL = "https://code.glowers.club/goyimnose/api-HydrusNetwork.git"
2026-01-22 03:52:07 -08:00
HYDRUS_INSTALLER_SCRIPT_URLS = (
2026-01-22 03:55:05 -08:00
"https://code.glowers.club/goyimnose/Medios-Macina/raw/branch/main/scripts/hydrusnetwork.py" ,
2026-01-22 03:52:07 -08:00
)
2026-01-21 22:52:52 -08:00
2026-01-12 13:51:26 -08:00
class ProgressBar :
def __init__ ( self , total : int , quiet : bool = False ):
self . total = total
self . current = 0
self . quiet = quiet
self . bar_width = 40
def update ( self , step_name : str ):
if self . current < self . total :
self . current += 1
if self . quiet :
return
2026-01-22 00:22:42 -08:00
2026-01-12 13:51:26 -08:00
percent = int ( 100 * ( self . current / self . total ))
filled = int ( self . bar_width * self . current // self . total )
bar = "█" * filled + "░" * ( self . bar_width - filled )
2026-07-10 21:44:26 -07:00
# Keep progress output append-only for terminals that do not reliably
# handle cursor-up ANSI control sequences.
print ( f "[ { bar } ] { percent : >3 } % | { step_name } " )
2026-01-12 13:51:26 -08:00
LOGO = r """
███╗ ███╗███████╗██████╗ ███████╗██╗ █████╗ ███╗ ███╗ █████╗ ██████╗██╗███╗ ██╗ █████╗
████╗ ████║██╔════╝██╔══██╗██╔════╝██║██╔══██╗ ████╗ ████║██╔══██╗██╔════╝██║████╗ ██║██╔══██╗
██╔████╔██║█████╗ ██║ ██║█████╗ ██║███████║ ██╔████╔██║███████║██║ ██║██╔██╗ ██║███████║
██║╚██╔╝██║██╔══╝ ██║ ██║██╔══╝ ██║██╔══██║ ██║╚██╔╝██║██╔══██║██║ ██║██║╚██╗██║██╔══██║
██║ ╚═╝ ██║███████╗██████╔╝███████╗██║██║ ██║ ██║ ╚═╝ ██║██║ ██║╚██████╗██║██║ ╚████║██║ ██║
╚═╝ ╚═╝╚══════╝╚═════╝ ╚══════╝╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝
2026-01-21 20:35:19 -08:00
< ΓΝΩΘΙ ΣΕΑΥΤΟΝ | TEMET NOSCE | KNOW THYSELF >
0123456789123456789123456789123456789123456789
0246813579246813579246813579246813579246813579
0369369369369369369369369369369369369369369369
0483726159483726159483726159483726159483726159
0516273849516273849516273849516273849516273849
0639639639639639639639639639639639639639639639
0753816429753816429753816429753816429753816429
0876543219876543219876543219876543219876543219
0999999999999999999999999999999999999999999999
< ALL WITHIN ARE KNOW | ABLE ALL ARE WITHOUT >
2026-01-12 13:51:26 -08:00
"""
2025-12-25 05:10:39 -08:00
# Helpers to find shell executables and to run the platform-specific
# bootstrap script (scripts/bootstrap.sh or scripts/bootstrap.ps1).
def _find_powershell () -> str | None :
for name in ( "pwsh" , "powershell" ):
p = shutil . which ( name )
if p :
return p
return None
def _find_shell () -> str | None :
for name in ( "bash" , "sh" ):
p = shutil . which ( name )
if p :
return p
return None
def run_platform_bootstrap ( repo_root : Path ) -> int :
"""Run the platform bootstrap script in quiet/non-interactive mode if present.
Returns the script exit code (0 on success). If no script is present this is a
no-op and returns 0.
"""
ps1 = repo_root / "scripts" / "bootstrap.ps1"
sh_script = repo_root / "scripts" / "bootstrap.sh"
system = platform . system () . lower ()
if system == "windows" and ps1 . exists ():
exe = _find_powershell ()
if not exe :
print ( "PowerShell not found; cannot run bootstrap.ps1" , file = sys . stderr )
return 1
2025-12-29 17:05:03 -08:00
cmd = [
exe ,
"-NoProfile" ,
"-NonInteractive" ,
"-ExecutionPolicy" ,
"Bypass" ,
"-File" ,
str ( ps1 ),
"-Quiet" ,
]
2025-12-25 05:10:39 -08:00
elif sh_script . exists ():
shell = _find_shell ()
if not shell :
print ( "Shell not found; cannot run bootstrap.sh" , file = sys . stderr )
return 1
# Use -q (quiet) to skip interactive prompts when supported.
cmd = [ shell , str ( sh_script ), "-q" ]
else :
# Nothing to run
return 0
print ( "Running platform bootstrap script:" , " " . join ( cmd ))
rc = subprocess . run ( cmd , cwd = str ( repo_root ))
if rc . returncode != 0 :
2025-12-29 18:42:02 -08:00
print (
f "Bootstrap script failed with exit code { rc . returncode } " ,
file = sys . stderr
)
2025-12-25 05:10:39 -08:00
return int ( rc . returncode or 0 )
2026-04-01 13:58:06 -07:00
def playwright_package_installed ( python_path : Optional [ Path ] = None ) -> bool :
interpreter = str ( python_path or sys . executable )
2025-12-25 05:10:39 -08:00
try :
2026-04-01 13:58:06 -07:00
result = subprocess . run (
[ interpreter , "-c" , "import playwright" ],
stdout = subprocess . DEVNULL ,
stderr = subprocess . DEVNULL ,
check = False ,
)
return result . returncode == 0
2025-12-25 05:10:39 -08:00
except Exception :
return False
def _build_playwright_install_cmd ( browsers : str | None ) -> list [ str ]:
"""Return the command to install Playwright browsers.
2026-01-09 13:41:18 -08:00
- If browsers is None or empty: default to install Chromium only (headless).
2025-12-25 05:10:39 -08:00
- If browsers contains 'all': install all engines by running 'playwright install' with no extra args.
- Otherwise, validate entries and return a command that installs the named engines.
2026-01-09 13:41:18 -08:00
The --with-deps flag is NOT used because:
1. The project already includes ffmpeg (in MPV/ffmpeg)
2. Most system dependencies should already be available
2025-12-25 05:10:39 -08:00
"""
2026-01-09 13:41:18 -08:00
# Use --skip-browsers to just install deps without browsers, then install specific browsers
2025-12-25 05:10:39 -08:00
base = [ sys . executable , "-m" , "playwright" , "install" ]
if not browsers :
return base + [ "chromium" ]
items = [ b . strip () . lower () for b in browsers . split ( "," ) if b . strip ()]
if not items :
return base + [ "chromium" ]
if "all" in items :
return base
2025-12-29 18:42:02 -08:00
allowed = { "chromium" ,
"firefox" ,
"webkit" }
2025-12-25 05:10:39 -08:00
invalid = [ b for b in items if b not in allowed ]
if invalid :
raise ValueError (
f "invalid browsers specified: { invalid } . Valid choices: chromium, firefox, webkit, or 'all'"
)
return base + items
2026-01-10 17:30:18 -08:00
def _check_deno_installed () -> bool :
"""Check if Deno is already installed and accessible in PATH."""
return shutil . which ( "deno" ) is not None
def _check_mpv_installed () -> bool :
"""Check if MPV is already installed and accessible in PATH."""
return shutil . which ( "mpv" ) is not None
def _install_mpv () -> int :
"""Install MPV player for the current platform.
Returns exit code 0 on success, non-zero otherwise.
"""
system = platform . system () . lower ()
try :
if system == "windows" :
# Windows: use winget (built-in package manager)
if shutil . which ( "winget" ):
print ( "Installing MPV via winget..." )
2026-07-10 21:17:41 -07:00
run (
[
"winget" ,
"install" ,
"--id=mpv.net" ,
"-e" ,
"--source" ,
"winget" ,
"--accept-source-agreements" ,
"--accept-package-agreements" ,
"--silent" ,
]
)
2026-01-10 17:30:18 -08:00
else :
print (
"MPV not found and winget not available. \n "
"Please install MPV manually from https://mpv.io/installation/" ,
file = sys . stderr
)
return 1
elif system == "darwin" :
# macOS: use Homebrew
if shutil . which ( "brew" ):
print ( "Installing MPV via Homebrew..." )
run ([ "brew" , "install" , "mpv" ])
else :
print (
"MPV not found and Homebrew not available. \n "
"Install Homebrew from https://brew.sh then run: brew install mpv" ,
file = sys . stderr
)
return 1
else :
# Linux: use apt, dnf, or pacman
if shutil . which ( "apt" ):
print ( "Installing MPV via apt..." )
run ([ "sudo" , "apt" , "install" , "-y" , "mpv" ])
elif shutil . which ( "dnf" ):
print ( "Installing MPV via dnf..." )
run ([ "sudo" , "dnf" , "install" , "-y" , "mpv" ])
elif shutil . which ( "pacman" ):
print ( "Installing MPV via pacman..." )
run ([ "sudo" , "pacman" , "-S" , "mpv" ])
else :
print (
"MPV not found and no recognized package manager available. \n "
"Please install MPV manually for your distribution." ,
file = sys . stderr
)
return 1
# Verify installation
if shutil . which ( "mpv" ):
print ( f "MPV installed at: { shutil . which ( 'mpv' ) } " )
return 0
print ( "MPV installation completed but 'mpv' not found in PATH." , file = sys . stderr )
return 1
except subprocess . CalledProcessError as exc :
print ( f "MPV install failed: { exc } " , file = sys . stderr )
return int ( exc . returncode or 1 )
except Exception as exc :
print ( f "MPV install error: { exc } " , file = sys . stderr )
return 1
2025-12-25 05:10:39 -08:00
def _install_deno ( version : str | None = None ) -> int :
"""Install Deno runtime for the current platform.
Uses the official Deno install scripts:
- Unix/macOS: curl -fsSL https://deno.land/x/install/install.sh | sh [-s <version>]
- Windows: powershell iwr https://deno.land/x/install/install.ps1 -useb | iex; Install-Deno [-Version <version>]
Returns exit code 0 on success, non-zero otherwise.
"""
system = platform . system () . lower ()
try :
if system == "windows" :
# Use official PowerShell installer
if version :
ver = version if version . startswith ( "v" ) else f "v { version } "
ps_cmd = f "iwr https://deno.land/x/install/install.ps1 -useb | iex; Install-Deno -Version { ver } "
else :
ps_cmd = "iwr https://deno.land/x/install/install.ps1 -useb | iex"
2025-12-29 18:42:02 -08:00
run (
[
"powershell" ,
"-NoProfile" ,
"-ExecutionPolicy" ,
"Bypass" ,
"-Command" ,
ps_cmd
]
)
2025-12-25 05:10:39 -08:00
else :
# POSIX: use curl + sh installer
if version :
ver = version if version . startswith ( "v" ) else f "v { version } "
cmd = f "curl -fsSL https://deno.land/x/install/install.sh | sh -s { ver } "
else :
cmd = "curl -fsSL https://deno.land/x/install/install.sh | sh"
run ([ "sh" , "-c" , cmd ])
# Check that 'deno' is now available in PATH
if shutil . which ( "deno" ):
print ( f "Deno installed at: { shutil . which ( 'deno' ) } " )
return 0
print (
"Deno installation completed but 'deno' not found in PATH. You may need to add Deno's bin directory to your PATH manually." ,
file = sys . stderr ,
)
return 1
except subprocess . CalledProcessError as exc :
print ( f "Deno install failed: { exc } " , file = sys . stderr )
return int ( exc . returncode or 1 )
def main () -> int :
2026-01-21 23:15:32 -08:00
# Ensure interactive stdin if piped
_ensure_interactive_stdin ()
2025-12-29 17:05:03 -08:00
parser = argparse . ArgumentParser (
description = "Bootstrap Medios-Macina: install deps and Playwright browsers"
2025-12-25 05:10:39 -08:00
)
parser . add_argument (
2025-12-29 17:05:03 -08:00
"--skip-deps" ,
action = "store_true" ,
2025-12-31 22:05:25 -08:00
help = "Skip installing Python dependencies from scripts/requirements.txt" ,
2025-12-25 05:10:39 -08:00
)
parser . add_argument (
2025-12-29 17:05:03 -08:00
"--no-playwright" ,
action = "store_true" ,
help = "Skip running 'playwright install' (only install packages)" ,
)
parser . add_argument (
"--playwright-only" ,
action = "store_true" ,
help = "Only run 'playwright install' (skips dependency installation)" ,
2025-12-25 05:10:39 -08:00
)
2025-12-31 16:10:35 -08:00
parser . add_argument (
"--no-delegate" ,
action = "store_true" ,
2026-04-01 13:58:06 -07:00
help = "Legacy no-op retained for wrapper compatibility; Python bootstrap is always the canonical entry point." ,
2025-12-31 16:10:35 -08:00
)
parser . add_argument (
"-q" ,
"--quiet" ,
action = "store_true" ,
help = "Quiet mode: minimize informational output (useful when called from platform wrappers)" ,
)
2025-12-25 05:10:39 -08:00
parser . add_argument (
"--browsers" ,
type = str ,
default = "chromium" ,
2025-12-29 18:42:02 -08:00
help =
"Comma-separated list of browsers to install: chromium,firefox,webkit or 'all' (default: chromium)" ,
2025-12-25 05:10:39 -08:00
)
parser . add_argument (
"--install-editable" ,
action = "store_true" ,
2026-01-09 16:57:27 -08:00
help = "Install the project in editable mode (pip install -e scripts) for running tests" ,
2025-12-25 05:10:39 -08:00
)
2026-01-10 17:30:18 -08:00
mpv_group = parser . add_mutually_exclusive_group ()
mpv_group . add_argument (
"--install-mpv" ,
action = "store_true" ,
help = "Install MPV player if not already installed (default behavior)" ,
)
mpv_group . add_argument (
"--no-mpv" ,
action = "store_true" ,
help = "Skip installing MPV player (opt out)"
)
2025-12-25 05:10:39 -08:00
deno_group = parser . add_mutually_exclusive_group ()
deno_group . add_argument (
2025-12-29 17:05:03 -08:00
"--install-deno" ,
action = "store_true" ,
help = "Install the Deno runtime (default behavior; kept for explicitness)" ,
)
deno_group . add_argument (
2025-12-29 18:42:02 -08:00
"--no-deno" ,
action = "store_true" ,
help = "Skip installing Deno runtime (opt out)"
2025-12-25 05:10:39 -08:00
)
parser . add_argument (
2025-12-29 17:05:03 -08:00
"--deno-version" ,
type = str ,
default = None ,
help = "Specific Deno version to install (e.g., v1.34.3)" ,
)
parser . add_argument (
"--upgrade-pip" ,
action = "store_true" ,
help = "Upgrade pip/setuptools/wheel before installing requirements" ,
2025-12-25 05:10:39 -08:00
)
2026-01-09 16:36:56 -08:00
parser . add_argument (
"--debug" ,
action = "store_true" ,
help = "Show detailed diagnostic information during installation" ,
)
parser . add_argument (
"--check-install" ,
action = "store_true" ,
help = "Verify that the 'mm' command was installed correctly" ,
)
2026-04-29 17:15:56 -07:00
parser . add_argument (
"--install-hydrus-web-gui" ,
action = "store_true" ,
help = "Clone/update api-HydrusNetwork, install npm dependencies, and prepare the Hydrus web GUI" ,
)
parser . add_argument (
"--hydrus-web-gui-root" ,
type = str ,
default = None ,
help = "Root directory where the Hydrus web GUI should be installed (default: the Medios repo root)" ,
)
parser . add_argument (
"--hydrus-web-gui-dest-name" ,
type = str ,
default = "api-HydrusNetwork" ,
help = "Folder name for the Hydrus web GUI checkout (default: api-HydrusNetwork)" ,
)
parser . add_argument (
"--install-hydrus-web-gui-service" ,
action = "store_true" ,
help = "Register the Hydrus web GUI to start automatically using systemd on Linux or Task Scheduler on Windows" ,
)
parser . add_argument (
"--setup-hydrus-web-gui-mpv-handler" ,
action = "store_true" ,
help = "Run the Hydrus web GUI's mpv-handler setup helper after installation (Windows/Linux)" ,
)
parser . add_argument (
"--hydrus-web-gui-service-name" ,
type = str ,
default = "hydrus-web-gui" ,
help = "Service name used for the Hydrus web GUI auto-start registration" ,
)
2025-12-31 16:10:35 -08:00
parser . add_argument (
"--uninstall" ,
action = "store_true" ,
help = "Uninstall local .venv and user shims (non-interactive)" ,
)
parser . add_argument (
"-y" ,
"--yes" ,
action = "store_true" ,
help = "Assume yes for confirmation prompts during uninstall" ,
)
2025-12-25 05:10:39 -08:00
args = parser . parse_args ()
2026-04-29 17:15:56 -07:00
if args . install_hydrus_web_gui_service or args . setup_hydrus_web_gui_mpv_handler :
args . install_hydrus_web_gui = True
2026-01-09 13:41:18 -08:00
# Ensure repo_root is always the project root, not the current working directory
# This prevents issues when bootstrap.py is run from different directories
2026-01-21 22:52:52 -08:00
try :
script_path = Path ( __file__ ) . resolve ()
script_dir = script_path . parent
repo_root = script_dir . parent
except NameError :
# Running via pipe/eval, __file__ is not defined
script_path = None
script_dir = Path . cwd ()
2026-01-22 00:22:42 -08:00
# In Web Installer mode, we don't assume CWD is the repo root.
repo_root = None
2026-01-21 22:52:52 -08:00
# DETECT REPOSITORY
# Check if we are already inside a valid Medios-Macina repo
2026-01-22 00:22:42 -08:00
def _is_valid_mm_repo ( p : Path | None ) -> bool :
if p is None : return False
2026-01-21 22:52:52 -08:00
return ( p / "CLI.py" ) . exists () and ( p / "scripts" ) . exists ()
2026-01-22 00:33:57 -08:00
# Detect if we are already inside a valid Medios-Macina repo
is_in_repo = _is_valid_mm_repo ( repo_root )
if not is_in_repo and _is_valid_mm_repo ( Path . cwd ()):
repo_root = Path . cwd ()
script_dir = repo_root / "scripts"
is_in_repo = True
# If running from a pipe/standalone (Web Installer Mode), we force is_in_repo = False
# so that Option 1 will always provide the path prompt rather than auto-detecting.
2026-01-21 23:30:19 -08:00
if script_path is None :
2026-01-22 00:33:57 -08:00
is_in_repo = False
2026-01-21 22:52:52 -08:00
2026-01-22 00:22:42 -08:00
if not args . quiet and args . debug :
2026-01-09 15:41:38 -08:00
print ( f "Bootstrap script location: { script_dir } " )
2026-01-22 00:22:42 -08:00
print ( f "Project root: { repo_root } " )
2026-01-09 15:41:38 -08:00
print ( f "Current working directory: { Path . cwd () } " )
2026-01-22 00:33:57 -08:00
print ( f "Is in repo: { is_in_repo } " )
2025-12-25 05:10:39 -08:00
2025-12-31 16:10:35 -08:00
# Helpers for interactive menu and uninstall detection
def _venv_python_path ( p : Path ) -> Path | None :
"""Return the path to a python executable inside a venv directory if present."""
if ( p / "Scripts" / "python.exe" ) . exists ():
return p / "Scripts" / "python.exe"
if ( p / "bin" / "python" ) . exists ():
return p / "bin" / "python"
return None
def _is_installed () -> bool :
"""Return True if the project appears installed into the local .venv."""
2026-01-22 00:22:42 -08:00
if repo_root is None :
return False
2025-12-31 16:10:35 -08:00
vdir = repo_root / ".venv"
py = _venv_python_path ( vdir )
if py is None :
return False
try :
2026-01-22 00:22:42 -08:00
# We use the global run() with check=False to avoid raising CalledProcessError
# 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
2025-12-31 16:10:35 -08:00
except Exception :
return False
def _do_uninstall () -> int :
2025-12-31 22:05:25 -08:00
"""Attempt to remove the local venv and any shims written to the user's bin.
If this script is running using the Python inside the local `.venv`, we
attempt to re-run the uninstall using a Python interpreter outside the
venv (so files can be removed on Windows). If no suitable external
interpreter can be found, the user is asked to deactivate the venv and
re-run the uninstall.
"""
2025-12-31 16:10:35 -08:00
vdir = repo_root / ".venv"
if not vdir . exists ():
if not args . quiet :
print ( "No local .venv found; nothing to uninstall." )
return 0
2025-12-31 22:05:25 -08:00
# If the current interpreter is the one inside the local venv, try to
# run the uninstall via a Python outside the venv so files (including
# the interpreter binary) can be removed on Windows.
2026-01-22 04:08:07 -08:00
current_exe = Path ( sys . executable ) . resolve ()
2025-12-31 22:05:25 -08:00
try :
in_venv = str ( current_exe ) . lower () . startswith ( str ( vdir . resolve ()) . lower ())
except Exception :
in_venv = False
if in_venv :
if not args . quiet :
print ( f "Detected local venv Python in use: { current_exe } " )
if not args . yes :
try :
resp = input ( "Uninstall will be attempted using a system Python outside the .venv. Continue? [Y/n]: " )
except EOFError :
print ( "Non-interactive environment; pass --uninstall --yes to uninstall without prompts." , file = sys . stderr )
return 2
if resp . strip () . lower () in ( "n" , "no" ):
print ( "Uninstall aborted." )
return 1
def _find_external_python () -> list [ str ] | None :
"""Return a command (list) for a Python interpreter outside the venv, or None."""
try :
base = Path ( sys . base_prefix )
candidates : list [ Path | str ] = []
if platform . system () . lower () == "windows" :
candidates . append ( base / "python.exe" )
else :
candidates . extend ([ base / "bin" / "python3" , base / "bin" / "python" ])
for name in ( "python3" , "python" ):
p = shutil . which ( name )
if p :
candidates . append ( Path ( p ))
# Special-case the Windows py launcher: ensure it resolves
# to a Python outside the venv before returning ['py','-3']
if platform . system () . lower () == "windows" :
py_launcher = shutil . which ( "py" )
if py_launcher :
try :
out = subprocess . check_output ([ "py" , "-3" , "-c" , "import sys; print(sys.executable)" ], text = True ) . strip ()
if out and not str ( Path ( out ) . resolve ()) . lower () . startswith ( str ( vdir . resolve ()) . lower ()):
return [ "py" , "-3" ]
except Exception :
pass
for c in candidates :
try :
if isinstance ( c , Path ) and c . exists ():
c_resolved = Path ( c ) . resolve ()
if not str ( c_resolved ) . lower () . startswith ( str ( vdir . resolve ()) . lower ()) and c_resolved != current_exe :
return [ str ( c_resolved )]
except Exception :
continue
except Exception :
pass
return None
ext = _find_external_python ()
if ext :
cmd = ext + [ str ( repo_root / "scripts" / "bootstrap.py" ), "--uninstall" , "--yes" ]
if not args . quiet :
print ( "Attempting uninstall using external Python:" , " " . join ( cmd ))
rc = subprocess . run ( cmd )
if rc . returncode != 0 :
print (
f "External uninstall exited with { rc . returncode } ; ensure no processes are using files in { vdir } and try again." ,
file = sys . stderr ,
)
return int ( rc . returncode or 0 )
print (
"Could not find a Python interpreter outside the local .venv. Please deactivate your venv (run 'deactivate') or run the uninstall from a system Python: \n python ./scripts/bootstrap.py --uninstall --yes" ,
file = sys . stderr ,
)
return 2
# Normal (non-venv) uninstall flow: confirm and remove launchers, shims, and venv
2025-12-31 16:10:35 -08:00
if not args . yes :
try :
prompt = input ( f "Remove local virtualenv at { vdir } and installed user shims? [y/N]: " )
except EOFError :
print ( "Non-interactive environment; pass --uninstall --yes to uninstall without prompts." , file = sys . stderr )
return 2
if prompt . strip () . lower () not in ( "y" , "yes" ):
print ( "Uninstall aborted." )
return 1
# Remove repo-local launchers
2025-12-31 22:05:25 -08:00
def _remove_launcher ( path : Path ) -> None :
if path . exists ():
2025-12-31 16:10:35 -08:00
try :
2025-12-31 22:05:25 -08:00
path . unlink ()
2025-12-31 16:10:35 -08:00
if not args . quiet :
2025-12-31 22:05:25 -08:00
print ( f "Removed local launcher: { path } " )
2025-12-31 16:10:35 -08:00
except Exception as exc :
2025-12-31 22:05:25 -08:00
print ( f "Warning: failed to remove { path } : { exc } " , file = sys . stderr )
scripts_launcher = repo_root / "scripts" / "mm.ps1"
_remove_launcher ( scripts_launcher )
for legacy in ( "mm" , "mm.ps1" , "mm.bat" ):
_remove_launcher ( repo_root / legacy )
2025-12-31 16:10:35 -08:00
# Remove user shims that the installer may have written
try :
system = platform . system () . lower ()
if system == "windows" :
user_bin = Path ( os . environ . get ( "USERPROFILE" , str ( Path . home ()))) / "bin"
if user_bin . exists ():
2025-12-31 22:05:25 -08:00
for name in ( "mm.ps1" ,):
2025-12-31 16:10:35 -08:00
p = user_bin / name
if p . exists ():
2025-12-31 22:05:25 -08:00
try :
p . unlink ()
if not args . quiet :
print ( f "Removed user shim: { p } " )
except Exception as exc :
print ( f "Warning: failed to remove { p } : { exc } " , file = sys . stderr )
2025-12-31 16:10:35 -08:00
else :
user_bin = Path ( os . environ . get ( "XDG_BIN_HOME" , str ( Path . home () / ".local/bin" )))
if user_bin . exists ():
p = user_bin / "mm"
if p . exists ():
p . unlink ()
if not args . quiet :
print ( f "Removed user shim: { p } " )
except Exception as exc :
print ( f "Warning: failed to remove user shims: { exc } " , file = sys . stderr )
# Remove .venv directory
try :
shutil . rmtree ( vdir )
if not args . quiet :
print ( f "Removed local virtualenv: { vdir } " )
except Exception as exc :
print ( f "Failed to remove venv: { exc } " , file = sys . stderr )
return 1
2025-12-25 05:10:39 -08:00
return 0
2026-01-11 12:28:16 -08:00
def _update_config_value ( root : Path , key : str , value : str ) -> bool :
2026-01-22 01:53:13 -08:00
db_path = root / "medios.db"
2026-01-11 12:28:16 -08:00
config_path = root / "config.conf"
2026-01-22 01:53:13 -08:00
# Try database first
if db_path . exists ():
try :
import sqlite3
2026-01-22 11:05:40 -08:00
with sqlite3 . connect ( str ( db_path ), timeout = 30.0 ) as conn :
2026-01-23 16:46:48 -08:00
conn . row_factory = sqlite3 . Row
2026-01-22 01:53:13 -08:00
cur = conn . cursor ()
2026-01-23 16:46:48 -08:00
2026-05-03 21:20:05 -07:00
# Find all existing hydrusnetwork instance names
2026-01-23 16:46:48 -08:00
cur . execute (
2026-05-03 21:20:05 -07:00
"SELECT DISTINCT item_name FROM config WHERE category='plugin' AND subtype='hydrusnetwork'"
2026-01-23 16:46:48 -08:00
)
rows = cur . fetchall ()
item_names = [ r [ 0 ] for r in rows if r [ 0 ]]
if not item_names :
# Only create if none exist. Use a sensible name from the path if possible.
# We don't have the hydrus_path here easily, but we can try to find it.
# For now, if we are in bootstrap, we might just be setting a global.
2026-05-03 21:20:05 -07:00
# But this function is specifically for instance settings.
2026-01-23 16:46:48 -08:00
# Let's use 'home' instead of 'hydrus' as it's the standard default.
item_name = "home"
2026-01-22 01:53:13 -08:00
cur . execute (
"INSERT OR REPLACE INTO config (category, subtype, item_name, key, value) VALUES (?, ?, ?, ?, ?)" ,
2026-05-03 21:20:05 -07:00
( 'plugin' , 'hydrusnetwork' , item_name , 'NAME' , item_name )
2026-01-22 01:53:13 -08:00
)
2026-01-23 16:46:48 -08:00
item_names = [ item_name ]
# Update all existing instances with this key/value
for name in item_names :
2026-01-22 01:53:13 -08:00
cur . execute (
"INSERT OR REPLACE INTO config (category, subtype, item_name, key, value) VALUES (?, ?, ?, ?, ?)" ,
2026-05-03 21:20:05 -07:00
( 'plugin' , 'hydrusnetwork' , name , key , value )
2026-01-22 01:53:13 -08:00
)
2026-01-23 16:46:48 -08:00
2026-01-22 01:53:13 -08:00
conn . commit ()
return True
except Exception as e :
print ( f "Error updating database config: { e } " )
# Fallback to config.conf
2026-01-11 12:28:16 -08:00
if not config_path . exists ():
fallback = root / "config.conf.remove"
if fallback . exists ():
shutil . copy ( fallback , config_path )
else :
return False
try :
content = config_path . read_text ( encoding = "utf-8" )
pattern = rf '^(\s* { re . escape ( key ) } \s*=\s*)(.*)$'
if re . search ( pattern , content , flags = re . MULTILINE ):
new_content = re . sub ( pattern , rf '\1" { value } "' , content , flags = re . MULTILINE )
else :
section_pattern = r '\[store=hydrusnetwork\]'
if re . search ( section_pattern , content ):
new_content = re . sub ( section_pattern , f '[store=hydrusnetwork] \n { key } =" { value } "' , content , count = 1 )
else :
new_content = content + f ' \n\n [store=hydrusnetwork] \n name="hydrus" \n { key } =" { value } "'
config_path . write_text ( new_content , encoding = "utf-8" )
return True
except Exception as e :
2026-01-22 01:53:13 -08:00
print ( f "Error updating legacy config: { e } " )
2026-01-11 12:28:16 -08:00
return False
2025-12-31 16:10:35 -08:00
def _interactive_menu () -> str | int :
2026-04-01 13:58:06 -07:00
"""Show a simple interactive menu to choose install/uninstall tasks."""
2025-12-31 16:10:35 -08:00
try :
installed = _is_installed ()
while True :
2026-01-11 12:28:16 -08:00
os . system ( "cls" if os . name == "nt" else "clear" )
2026-01-22 00:22:42 -08:00
term_width = shutil . get_terminal_size (( 80 , 20 )) . columns
# 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 )
2026-01-11 11:11:32 -08:00
2026-01-12 16:15:51 -08:00
print ( " \n " * 2 )
for line in logo_lines :
2026-01-22 00:22:42 -08:00
print ( f " { logo_padding }{ line . rstrip () } " )
2026-01-12 16:15:51 -08:00
print ( " \n " )
menu_title = " MEDEIA MACINA BOOTSTRAP MENU "
border = "=" * len ( menu_title )
print ( border . center ( term_width ))
print ( menu_title . center ( term_width ))
print ( border . center ( term_width ))
print ( " \n " )
2026-01-22 00:22:42 -08:00
# Define menu options
2026-04-29 17:15:56 -07:00
service_option = (
"5) Install Hydrus System Service (Auto-update + Headless)"
if installed
else "4) Install Hydrus System Service (Auto-update + Headless)"
)
2026-01-22 00:22:42 -08:00
options = [
"1) Reinstall" if installed else "1) Install" ,
2026-04-29 17:15:56 -07:00
"2) Extras > HydrusNetwork" ,
"3) Extras > Hydrus Web GUI" ,
2026-01-22 00:22:42 -08:00
]
2026-01-12 16:15:51 -08:00
if installed :
2026-04-29 17:15:56 -07:00
options . append ( "4) Uninstall" )
options . append ( service_option )
2026-01-22 00:22:42 -08:00
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 } " )
2026-01-12 16:15:51 -08:00
prompt = " \n Choose an option: "
# Try to center the prompt roughly
2026-01-22 00:22:42 -08:00
indent = " " * max (( term_width // 2 ) - ( len ( prompt ) // 2 ), 0 )
sys . stdout . write ( f " { indent }{ prompt } " )
sys . stdout . flush ()
choice = sys . stdin . readline () . strip () . lower ()
2026-01-11 11:11:32 -08:00
if choice in ( "1" , "install" , "reinstall" ):
return "install"
2026-01-11 12:28:16 -08:00
if choice in ( "2" , "extras" , "hydrus" ):
return "extras_hydrus"
2026-04-29 17:15:56 -07:00
if choice in ( "3" , "web" , "webgui" , "gui" ):
return "extras_hydrus_web_gui"
2026-01-11 11:11:32 -08:00
2026-04-29 17:15:56 -07:00
if installed and choice in ( "4" , "uninstall" ):
2026-01-11 11:11:32 -08:00
return "uninstall"
2026-04-29 17:15:56 -07:00
if ( installed and choice == "5" ) or ( not installed and choice == "4" ):
2026-01-22 00:22:42 -08:00
return "install_service"
2026-01-11 11:11:32 -08:00
if choice in ( "q" , "quit" , "exit" ):
return 0
2025-12-31 16:10:35 -08:00
except EOFError :
2026-04-01 13:58:06 -07:00
return "install"
2025-12-31 16:10:35 -08:00
2026-01-22 03:39:28 -08:00
def _prompt_hydrus_install_location () -> tuple [ Path , str ] | None :
"""Ask the user for the Hydrus installation root and folder name."""
default_root = Path . home ()
default_dest_name = "hydrusnetwork"
print ( " \n [Standalone Hydrus Installation]" )
print ( "Choose where to install HydrusNetwork (the installer will create the repo there)." )
try :
root_input = input ( f "Root directory [ { default_root } ]: " ) . strip ()
if root_input :
if len ( root_input ) == 2 and root_input [ 1 ] == ":" and root_input [ 0 ] . isalpha ():
root_input += " \\ "
expanded = os . path . expandvars ( os . path . expanduser ( root_input ))
root_path = Path ( expanded ) . resolve ()
else :
root_path = default_root
dest_input = input ( f "Folder name [ { default_dest_name } ]: " ) . strip ()
dest_name = dest_input or default_dest_name
return root_path , dest_name
except ( EOFError , KeyboardInterrupt ):
print ( " \n Hydrus installation cancelled." )
return None
2026-04-29 17:15:56 -07:00
def _prompt_hydrus_web_gui_location () -> tuple [ Path , str , bool , bool ] | None :
"""Ask the user for the Hydrus web GUI installation settings."""
default_root = repo_root if repo_root else Path . home ()
default_dest_name = "api-HydrusNetwork"
print ( " \n [Hydrus Web GUI Installation]" )
print ( "Install the api-HydrusNetwork web UI and optionally register it to auto-start." )
try :
root_input = input ( f "Root directory [ { default_root } ]: " ) . strip ()
if root_input :
if len ( root_input ) == 2 and root_input [ 1 ] == ":" and root_input [ 0 ] . isalpha ():
root_input += " \\ "
root_path = Path ( os . path . expandvars ( os . path . expanduser ( root_input ))) . resolve ()
else :
root_path = default_root
dest_input = input ( f "Folder name [ { default_dest_name } ]: " ) . strip ()
dest_name = dest_input or default_dest_name
service_default = "Y"
service_input = input (
"Install auto-start service with systemd/Task Scheduler? [Y/n]: "
) . strip () . lower ()
install_service = service_input not in { "n" , "no" }
setup_mpv_input = input (
"Run mpv-handler setup after install? [y/N]: "
) . strip () . lower ()
setup_mpv_handler = setup_mpv_input in { "y" , "yes" }
return root_path , dest_name , install_service , setup_mpv_handler
except ( EOFError , KeyboardInterrupt ):
print ( " \n Hydrus web GUI installation cancelled." )
return None
2026-01-22 03:31:36 -08:00
def _clone_repo ( url : str , dest : Path , depth : int = 1 ) -> bool :
"""Helper to clone a repository."""
try :
2026-04-01 13:58:06 -07:00
cmd = [ "git" , "clone" ]
2026-01-22 03:31:36 -08:00
if depth :
cmd . extend ([ "--depth" , str ( depth )])
2026-04-01 13:58:06 -07:00
cmd . extend ([ url , str ( dest )])
2026-01-22 03:31:36 -08:00
subprocess . check_call ( cmd )
return True
except Exception as e :
print ( f "Error: Failed to clone repository: { e } " , file = sys . stderr )
return False
2026-01-22 03:47:01 -08:00
def _download_hydrus_installer ( dest : Path ) -> bool :
"""Download the hydrusnetwork.py helper script into the provided path."""
2026-01-22 03:52:07 -08:00
last_exc : Exception | None = None
for url in HYDRUS_INSTALLER_SCRIPT_URLS :
try :
2026-01-22 03:55:05 -08:00
# Add a user-agent to avoid being blocked by some servers
req = urllib . request . Request ( url , headers = { "User-Agent" : "Medeia-Macina-Installer" })
with urllib . request . urlopen ( req ) as response :
2026-01-22 03:52:07 -08:00
dest . write_bytes ( response . read ())
return True
except Exception as exc :
last_exc = exc
if last_exc :
print ( f "Error: Failed to download Hydrus installer script: { last_exc } " , file = sys . stderr )
else :
print ( "Error: Failed to download Hydrus installer script" , file = sys . stderr )
return False
2026-01-22 03:47:01 -08:00
2026-04-29 17:15:56 -07:00
def _run_hydrus_web_gui_installer (
install_root : Path ,
install_dest : str ,
* ,
install_service : bool = False ,
setup_mpv_handler : bool = False ,
) -> bool :
web_gui_script = script_dir / "hydrus_web_gui.py"
if not web_gui_script . exists ():
print ( f "Error: { web_gui_script } not found." , file = sys . stderr )
return False
cmd = [
sys . executable ,
str ( web_gui_script ),
"--root" ,
str ( install_root ),
"--dest-name" ,
install_dest ,
"--repo" ,
HYDRUS_WEB_GUI_REPO_URL ,
]
if install_service :
cmd . extend ([
"--install-service" ,
"--service-name" ,
args . hydrus_web_gui_service_name ,
])
if setup_mpv_handler :
cmd . append ( "--setup-mpv-handler" )
if args . debug :
cmd . append ( "--debug" )
try :
subprocess . check_call ( cmd , stdin = sys . stdin )
return True
except subprocess . CalledProcessError :
return False
2026-01-21 23:21:47 -08:00
def _ensure_repo_available () -> bool :
"""Prompt for a clone location when running outside the repository."""
nonlocal repo_root , script_dir , is_in_repo
2026-04-30 18:56:22 -07:00
repo_dir_name = "Medios-Macina"
2026-01-21 23:21:47 -08:00
2026-01-22 00:22:42 -08:00
# If we have already settled on a repository path in this session, skip.
if is_in_repo and repo_root is not None :
2026-01-21 23:21:47 -08:00
return True
if not shutil . which ( "git" ):
print ( " \n Error: 'git' was not found on your PATH." , file = sys . stderr )
print ( "Please install Git (https://git-scm.com/) and try again." , file = sys . stderr )
return False
try :
2026-01-21 23:39:59 -08:00
# When piped, script_path is None. We don't want to use the detected repo_root
# because that's just CWD.
2026-01-22 04:08:07 -08:00
if script_path is not None and repo_root is not None :
2026-01-21 23:21:47 -08:00
default_install = repo_root
else :
2026-01-22 03:20:06 -08:00
# When piped, default to home folder on POSIX, CWD on Windows
if platform . system () . lower () != "windows" :
default_install = Path . home () / "medios"
else :
default_install = Path . cwd () / "Medios-Macina"
2026-01-21 23:21:47 -08:00
print ( " \n [WEB INSTALLER MODE]" )
2026-01-21 23:27:48 -08:00
print ( f "Current working directory: { Path . cwd () } " )
2026-01-21 23:21:47 -08:00
print ( "Where would you like to install Medios-Macina?" )
2026-01-21 23:27:48 -08:00
# Use sys.stdin.readline() to be more robust than input() in some terminal environments
sys . stdout . write ( f "Installation directory [ { default_install } ]: " )
sys . stdout . flush ()
install_dir_raw = sys . stdin . readline () . strip ()
2026-01-21 23:21:47 -08:00
if not install_dir_raw :
install_path = default_install
else :
2026-01-22 03:20:06 -08:00
# Resolve while expanding user paths (~) and environment variables ($HOME)
expanded = os . path . expandvars ( os . path . expanduser ( install_dir_raw ))
install_path = Path ( expanded ) . resolve ()
2026-01-22 04:08:07 -08:00
if install_path is None :
print ( "Error: Could not determine installation path." , file = sys . stderr )
return False
2026-04-30 18:56:22 -07:00
if install_path . exists () and install_path . is_dir () and not _is_valid_mm_repo ( install_path ):
try :
has_contents = any ( install_path . iterdir ())
except Exception :
has_contents = False
if has_contents :
install_path = install_path / repo_dir_name
2026-01-21 23:21:47 -08:00
except ( EOFError , KeyboardInterrupt ):
return False
if not install_path . exists ():
print ( f "Creating directory: { install_path } " )
install_path . mkdir ( parents = True , exist_ok = True )
if _is_valid_mm_repo ( install_path ):
if not args . quiet :
print ( f "Using existing repository in { install_path } ." )
repo_root = install_path
else :
print ( f "Cloning Medios-Macina into { install_path } (depth 1)..." )
print ( f "Source: { REPO_URL } " )
2026-01-22 03:31:36 -08:00
if _clone_repo ( REPO_URL , install_path , depth = 1 ):
2026-01-21 23:21:47 -08:00
repo_root = install_path
2026-01-22 03:31:36 -08:00
else :
2026-01-21 23:21:47 -08:00
return False
os . chdir ( str ( repo_root ))
2026-01-22 00:22:42 -08:00
is_in_repo = True
2026-01-21 23:21:47 -08:00
if not args . quiet :
print ( f " \n Successfully set up repository at { repo_root } " )
print ( "Resuming bootstrap... \n " )
script_dir = repo_root / "scripts"
is_in_repo = True
return True
2025-12-31 16:10:35 -08:00
# If the user passed --uninstall explicitly, perform non-interactive uninstall and exit
if args . uninstall :
return _do_uninstall ()
2026-01-09 16:36:56 -08:00
if args . check_install :
# Verify mm command is properly installed
home = Path . home ()
system = platform . system () . lower ()
print ( "Checking 'mm' command installation..." )
print ()
if system == "windows" :
user_bin = Path ( os . environ . get ( "USERPROFILE" , str ( home ))) / "bin"
2026-01-10 15:04:16 -08:00
mm_bat = user_bin / "mm.bat"
2026-01-09 16:36:56 -08:00
2026-01-19 03:14:30 -08:00
print ( "Checking for shim files:" )
2026-01-10 15:04:16 -08:00
print ( f " mm.bat: { '✓' if mm_bat . exists () else '✗' } ( { mm_bat } )" )
2026-01-09 16:36:56 -08:00
print ()
2026-01-10 15:04:16 -08:00
if mm_bat . exists ():
bat_content = mm_bat . read_text ( encoding = "utf-8" )
if "REPO=" in bat_content or "ENTRY=" in bat_content :
print ( f " mm.bat content looks valid ( { len ( bat_content ) } bytes)" )
2026-01-09 16:36:56 -08:00
else :
2026-01-19 03:14:30 -08:00
print ( " ⚠️ mm.bat content may be corrupted" )
2026-01-09 16:36:56 -08:00
print ()
# Check PATH
path = os . environ . get ( "PATH" , "" )
user_bin_str = str ( user_bin )
in_path = user_bin_str in path
2026-01-19 03:14:30 -08:00
print ( "Checking PATH environment variable:" )
2026-01-09 16:36:56 -08:00
print ( f " { user_bin_str } in current session PATH: { '✓' if in_path else '✗' } " )
# Check registry
try :
import winreg
reg = winreg . ConnectRegistry ( None , winreg . HKEY_CURRENT_USER )
key = winreg . OpenKey ( reg , "Environment" , 0 , winreg . KEY_READ )
current_path = winreg . QueryValueEx ( key , "Path" )[ 0 ]
winreg . CloseKey ( key )
in_reg = user_bin_str in current_path
print ( f " { user_bin_str } in registry PATH: { '✓' if in_reg else '✗' } " )
if not in_reg :
print ()
print ( "📝 Note: Path is not in registry. It may work in this session but won't persist." )
print ( f " To fix, run: [Environment]::SetEnvironmentVariable('PATH', ' { user_bin_str } ;' + [Environment]::GetEnvironmentVariable('PATH','User'), 'User')" )
except Exception as e :
print ( f " Could not check registry: { e } " )
print ()
# Test if mm command works
print ( "Testing 'mm' command..." )
try :
result = subprocess . run ([ "mm" , "--help" ], capture_output = True , text = True , timeout = 5 )
if result . returncode == 0 :
2026-01-19 03:14:30 -08:00
print ( " ✓ 'mm --help' works!" )
2026-01-09 16:36:56 -08:00
print ( f " Output (first line): { result . stdout . split ( chr ( 10 ))[ 0 ] } " )
else :
print ( f " ✗ 'mm --help' failed with exit code { result . returncode } " )
if result . stderr :
print ( f " Error: { result . stderr . strip () } " )
except FileNotFoundError :
# mm not found via PATH, try calling the .ps1 directly
2026-01-19 03:14:30 -08:00
print ( " ✗ 'mm' command not found in PATH" )
print ( " Shims exist but command is not accessible via PATH" )
2026-01-09 16:36:56 -08:00
print ()
print ( "Attempting to call shim directly..." )
try :
result = subprocess . run (
2026-01-09 17:06:48 -08:00
[ str ( mm_bat ), "--help" ],
2026-01-09 16:36:56 -08:00
capture_output = True , text = True , timeout = 5
)
if result . returncode == 0 :
2026-01-19 03:14:30 -08:00
print ( " ✓ Direct shim call works!" )
print ( " The shim files are valid and functional." )
2026-01-09 16:36:56 -08:00
print ()
print ( "⚠️ 'mm' is not in PATH, but the shims are working correctly." )
print ()
print ( "Possible causes and fixes:" )
2026-01-19 03:14:30 -08:00
print ( " 1. Terminal needs restart: Close and reopen your terminal/PowerShell" )
print ( " 2. PATH reload: Run: $env:Path = [Environment]::GetEnvironmentVariable('PATH', 'User') + ';' + [Environment]::GetEnvironmentVariable('PATH', 'Machine')" )
2026-01-09 17:06:48 -08:00
print ( f " 3. Manual PATH: Add { user_bin } to your system PATH manually" )
2026-01-09 16:36:56 -08:00
else :
2026-01-19 03:14:30 -08:00
print ( " ✗ Direct shim call failed" )
2026-01-09 16:36:56 -08:00
if result . stderr :
print ( f " Error: { result . stderr . strip () } " )
except Exception as e :
print ( f " ✗ Could not test direct shim: { e } " )
except subprocess . TimeoutExpired :
2026-01-19 03:14:30 -08:00
print ( " ✗ 'mm' command timed out" )
2026-01-09 16:36:56 -08:00
except Exception as e :
print ( f " ✗ Error testing 'mm': { e } " )
else :
# POSIX (Linux/macOS)
2026-01-10 23:20:00 -08:00
# Check likely installation locations
locations = [ home / ".local" / "bin" / "mm" , Path ( "/usr/local/bin/mm" ), Path ( "/usr/bin/mm" )]
found_shims = [ p for p in locations if p . exists ()]
2026-01-09 16:36:56 -08:00
2026-01-19 03:14:30 -08:00
print ( "Checking for shim files:" )
2026-01-10 23:20:00 -08:00
for p in locations :
if p . exists ():
print ( f " mm: ✓ ( { p } )" )
else :
if args . debug :
print ( f " mm: ✗ ( { p } )" )
if not found_shims :
2026-01-19 03:14:30 -08:00
print ( " mm: ✗ (No shim found in standard locations)" )
2026-01-09 16:36:56 -08:00
print ()
path = os . environ . get ( "PATH" , "" )
2026-01-10 23:20:00 -08:00
# Find which 'mm' is actually being run
actual_mm = shutil . which ( "mm" )
2026-01-19 03:14:30 -08:00
print ( "Checking PATH environment variable:" )
2026-01-10 23:20:00 -08:00
if actual_mm :
print ( f " 'mm' resolved to: { actual_mm } " )
# Check if it's in a directory on the PATH
if any ( str ( Path ( actual_mm ) . parent ) in p for p in path . split ( os . pathsep )):
2026-01-19 03:14:30 -08:00
print ( " Command is accessible via current session PATH: ✓" )
2026-01-10 23:20:00 -08:00
else :
2026-01-19 03:14:30 -08:00
print ( " Command is found but directory may not be in current PATH: ⚠️" )
2026-01-10 23:20:00 -08:00
else :
2026-01-19 03:14:30 -08:00
print ( " 'mm' not found in current session PATH: ✗" )
2026-01-09 16:36:56 -08:00
print ()
# Test if mm command works
print ( "Testing 'mm' command..." )
try :
result = subprocess . run ([ "mm" , "--help" ], capture_output = True , text = True , timeout = 5 )
if result . returncode == 0 :
2026-01-19 03:14:30 -08:00
print ( " ✓ 'mm --help' works!" )
2026-01-09 16:36:56 -08:00
print ( f " Output (first line): { result . stdout . split ( chr ( 10 ))[ 0 ] } " )
else :
print ( f " ✗ 'mm --help' failed with exit code { result . returncode } " )
if result . stderr :
print ( f " Error: { result . stderr . strip () } " )
except FileNotFoundError :
2026-01-19 03:14:30 -08:00
print ( " ✗ 'mm' command not found in PATH" )
2026-01-09 16:36:56 -08:00
except Exception as e :
print ( f " ✗ Error testing 'mm': { e } " )
print ()
print ( "✅ Installation check complete!" )
return 0
2026-01-11 11:16:54 -08:00
2026-01-21 23:15:32 -08:00
# If no specific action flag is passed and we're in a terminal (or we're being piped), show the menu
if ( sys . stdin . isatty () or sys . stdout . isatty () or script_path is None ) and not args . quiet :
2026-01-22 04:08:07 -08:00
while True :
sel = _interactive_menu ()
if sel == "install" :
if not _ensure_repo_available ():
return 1
args . skip_deps = False
args . install_editable = True
args . no_playwright = False
# Break the loop to proceed with the main installation steps below
break
elif sel == "extras_hydrus" :
install_location = _prompt_hydrus_install_location ()
if install_location is None :
continue
install_root , install_dest = install_location
# Choice 2 is for installing HydrusNetwork standalone/independently.
# We preferentially use the local script if already in a repo.
hydrus_script = None
temp_installer_path : Path | None = None
temp_hydrus_repo : Path | None = None
if is_in_repo and repo_root :
hydrus_script = repo_root / "scripts" / "hydrusnetwork.py"
if not hydrus_script or not hydrus_script . exists ():
print ( "Downloading the Hydrus installation helper..." )
try :
fd , path = tempfile . mkstemp ( prefix = "mm_hydrus_" , suffix = ".py" )
os . close ( fd )
helper_path = Path ( path )
if _download_hydrus_installer ( helper_path ):
hydrus_script = helper_path
temp_installer_path = helper_path
else :
helper_path . unlink ( missing_ok = True )
hydrus_script = None
except Exception as e :
print ( f "Error setting up temporary installer: { e } " )
2026-01-22 03:52:07 -08:00
hydrus_script = None
2026-01-22 04:08:07 -08:00
if ( not hydrus_script or not hydrus_script . exists ()) and temp_hydrus_repo is None :
print ( "Falling back to clone the Medios-Macina repository to obtain the helper script..." )
try :
temp_mm_repo_dir = Path ( tempfile . mkdtemp ( prefix = "mm_repo_" ))
if _clone_repo ( REPO_URL , temp_mm_repo_dir , depth = 1 ):
hydrus_script = temp_mm_repo_dir / "scripts" / "hydrusnetwork.py"
temp_hydrus_repo = temp_mm_repo_dir
else :
shutil . rmtree ( temp_mm_repo_dir , ignore_errors = True )
hydrus_script = None
except Exception as e :
print ( f "Error cloning Medios-Macina repo: { e } " )
2026-01-22 03:52:07 -08:00
hydrus_script = None
2026-01-22 00:33:57 -08:00
2026-01-22 04:08:07 -08:00
if hydrus_script and hydrus_script . exists ():
try :
# Clear out project-venv related env vars to prevent auto-reexec
env = os . environ . copy ()
env . pop ( "VIRTUAL_ENV" , None )
env . pop ( "PYTHONHOME" , None )
env . pop ( "PYTHONPATH" , None )
# We use sys.executable (the one running bootstrap.py) to run hydrusnetwork.py
# This ensures it uses the same environment that started the bootstrap.
# Pass sys.stdin to ensure the subprocess can talk to the terminal.
subprocess . check_call (
[
sys . executable ,
str ( hydrus_script ),
"--no-project-venv" ,
"--root" ,
str ( install_root ),
"--dest-name" ,
install_dest ,
],
env = env ,
stdin = sys . stdin
)
# Update the main project's config with the new Hydrus path
if is_in_repo and repo_root :
_update_config_value ( repo_root , "gitclone" , str ( Path ( install_root ) / install_dest ))
except subprocess . CalledProcessError :
print ( " \n HydrusNetwork setup exited with an error." )
except Exception as e :
print ( f " \n Failed to run HydrusNetwork setup: { e } " )
finally :
if temp_installer_path :
temp_installer_path . unlink ( missing_ok = True )
if temp_hydrus_repo is not None :
shutil . rmtree ( temp_hydrus_repo , ignore_errors = True )
else :
print ( f " \n Error: { hydrus_script } not found." )
print ( " \n Hydrus installation task finished." )
sys . stdout . write ( "Press Enter to return to menu..." )
sys . stdout . flush ()
sys . stdin . readline ()
continue
2026-04-29 17:15:56 -07:00
elif sel == "extras_hydrus_web_gui" :
if not _ensure_repo_available ():
return 1
web_gui_location = _prompt_hydrus_web_gui_location ()
if web_gui_location is None :
continue
install_root , install_dest , install_service , setup_mpv_handler = web_gui_location
ok = _run_hydrus_web_gui_installer (
install_root ,
install_dest ,
install_service = install_service ,
setup_mpv_handler = setup_mpv_handler ,
)
if ok :
print ( " \n Hydrus web GUI installation finished." )
else :
print ( " \n Hydrus web GUI installation failed." )
sys . stdout . write ( "Press Enter to return to menu..." )
sys . stdout . flush ()
sys . stdin . readline ()
continue
2026-01-22 04:08:07 -08:00
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
2026-01-22 00:22:42 -08:00
2026-01-22 04:08:07 -08:00
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 ( " \n Press Enter to return to menu..." )
sys . stdout . flush ()
sys . stdin . readline ()
continue
2026-01-23 02:57:27 -08:00
run_client_script = Path ( __file__ ) . parent / "run_client.py"
if not run_client_script . exists ():
# Fallback to target repo's copy if our local one is missing
candidates = [
target_repo / "run_client.py" ,
target_repo / "scripts" / "run_client.py" ,
]
for candidate in candidates :
if candidate . exists ():
run_client_script = candidate
break
2026-01-22 04:08:07 -08:00
2026-01-23 02:57:27 -08:00
if run_client_script and run_client_script . exists ():
2026-01-22 04:08:07 -08:00
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" ,
2026-01-23 03:48:41 -08:00
"--service-user" , "hydrusnetwork" ,
2026-01-22 04:08:07 -08:00
"--repo-root" , str ( target_repo ),
"--headless" ,
"--pull"
],
stdin = sys . stdin
)
print ( " \n Hydrus System service installed successfully." )
except subprocess . CalledProcessError :
print ( " \n Service installation failed." )
except Exception as e :
print ( f " \n Error installing service: { e } " )
else :
print ( f " \n Error: { run_client_script } not found." )
2026-01-22 00:22:42 -08:00
sys . stdout . write ( " \n Press Enter to return to menu..." )
sys . stdout . flush ()
sys . stdin . readline ()
2026-01-22 04:08:07 -08:00
continue
elif sel == "uninstall" :
return _do_uninstall ()
elif sel == 0 :
return 0
elif sel == "menu" :
continue
2026-04-01 13:58:06 -07:00
if not is_in_repo :
if not _ensure_repo_available ():
return 1
2025-12-31 16:10:35 -08:00
2025-12-25 05:10:39 -08:00
if sys . version_info < ( 3 , 8 ):
print ( "Warning: Python 3.8+ is recommended." , file = sys . stderr )
2026-01-12 13:51:26 -08:00
# UI setup: Logo and Progress Bar
if not args . quiet and not args . debug :
2026-01-12 14:16:46 -08:00
# Clear the terminal before showing logo
os . system ( 'cls' if os . name == 'nt' else 'clear' )
2026-01-12 16:15:51 -08:00
term_width = shutil . get_terminal_size (( 80 , 20 )) . columns
2026-01-21 20:35:19 -08:00
logo_lines = LOGO . strip ( ' \n ' ) . splitlines ()
max_line_width = 0
for line in logo_lines :
max_line_width = max ( max_line_width , len ( line . rstrip ()))
padding = ' ' * max (( term_width - max_line_width ) // 2 , 0 )
2026-01-12 16:15:51 -08:00
print ( " \n " * 2 )
for line in logo_lines :
2026-01-21 20:35:19 -08:00
print ( f " { padding }{ line . rstrip () } " )
2026-01-12 16:15:51 -08:00
print ( " \n " )
2026-01-12 13:51:26 -08:00
2026-01-22 00:22:42 -08:00
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
2026-04-01 13:58:06 -07:00
should_install_deps = not args . skip_deps and not args . playwright_only
should_install_playwright = not args . no_playwright
should_install_project = not args . playwright_only
total_steps = 2
if args . playwright_only :
total_steps += 1
else :
if args . upgrade_pip :
total_steps += 1
if should_install_deps :
total_steps += 1
if should_install_playwright :
total_steps += 1
if should_install_project :
total_steps += 1
total_steps += 3
if not getattr ( args , "no_mpv" , False ):
total_steps += 1
if not getattr ( args , "no_deno" , False ):
total_steps += 1
2026-04-29 17:15:56 -07:00
if args . install_hydrus_web_gui :
total_steps += 1
2026-01-12 13:51:26 -08:00
pb = ProgressBar ( total_steps , quiet = args . quiet or args . debug )
2026-07-10 21:44:26 -07:00
def _run_cmd ( cmd : list [ str ], cwd : Optional [ Path ] = None , context : str = "" ):
2026-01-12 13:51:26 -08:00
"""Helper to run commands with shared settings."""
2026-07-10 21:44:26 -07:00
if not args . quiet and context :
print ( f "[working] { context } " )
# In normal mode, stream subprocess output so users can see activity.
run ( cmd , quiet = ( args . quiet and not args . debug ), debug = args . debug , cwd = cwd )
2026-01-12 13:51:26 -08:00
2025-12-25 05:10:39 -08:00
# Opinionated: always create or use a local venv at the project root (.venv)
venv_dir = repo_root / ".venv"
2026-01-09 15:41:38 -08:00
# Validate that venv_dir is where we expect it to be
if not args . quiet :
print ( f "Planned venv location: { venv_dir } " )
if venv_dir . parent != repo_root :
print ( f "WARNING: venv parent is { venv_dir . parent } , expected { repo_root } " , file = sys . stderr )
if "scripts" in str ( venv_dir ) . lower ():
print ( f "WARNING: venv path contains 'scripts': { venv_dir } " , file = sys . stderr )
2025-12-25 05:10:39 -08:00
2026-01-12 13:51:26 -08:00
def _venv_python_bin ( p : Path ) -> Path :
2025-12-25 05:10:39 -08:00
if platform . system () . lower () == "windows" :
return p / "Scripts" / "python.exe"
return p / "bin" / "python"
def _ensure_local_venv () -> Path :
"""Create (if missing) and return the path to the venv's python executable."""
try :
if not venv_dir . exists ():
2026-01-12 13:51:26 -08:00
_run_cmd ([ sys . executable , "-m" , "venv" , str ( venv_dir )])
py = _venv_python_bin ( venv_dir )
2025-12-25 05:10:39 -08:00
if not py . exists ():
2026-01-12 13:51:26 -08:00
_run_cmd ([ sys . executable , "-m" , "venv" , str ( venv_dir )])
py = _venv_python_bin ( venv_dir )
2025-12-25 05:10:39 -08:00
if not py . exists ():
raise RuntimeError ( f "Unable to locate venv python at { py } " )
return py
except subprocess . CalledProcessError as exc :
print ( f "Failed to create or prepare local venv: { exc } " , file = sys . stderr )
raise
2025-12-31 22:05:25 -08:00
def _ensure_pip_available ( python_path : Path ) -> None :
"""Ensure pip is available inside the venv; fall back to ensurepip if needed."""
try :
2026-01-22 00:22:42 -08:00
# Use run() to ensure clean environment (fixes ModuleNotFoundError: No module named 'attr' in pipe mode)
run ([ str ( python_path ), "-m" , "pip" , "--version" ], quiet = True )
2025-12-31 22:05:25 -08:00
return
except Exception :
pass
try :
2026-01-22 00:22:42 -08:00
# ensurepip is a stdlib module, but it can still benefit from a clean environment
# although usually it doesn't use site-packages.
run ([ str ( python_path ), "-m" , "ensurepip" , "--upgrade" ], quiet = True )
except Exception :
2025-12-31 22:05:25 -08:00
print (
"Failed to install pip inside the local virtualenv via ensurepip; ensure your Python build includes ensurepip and retry." ,
file = sys . stderr ,
)
raise
2025-12-25 05:10:39 -08:00
2026-01-12 13:51:26 -08:00
# 1. Virtual Environment Setup
pb . update ( "Preparing virtual environment..." )
2025-12-25 05:10:39 -08:00
venv_python = _ensure_local_venv ()
2026-01-12 13:51:26 -08:00
# 2. Pip Availability
pb . update ( "Checking for pip..." )
2025-12-31 22:05:25 -08:00
_ensure_pip_available ( venv_python )
2025-12-25 05:10:39 -08:00
try :
if args . playwright_only :
2026-01-12 13:51:26 -08:00
# Playwright browser install (short-circuit)
2026-04-01 13:58:06 -07:00
pb . update ( "Setting up Playwright and browsers..." )
if not playwright_package_installed ( venv_python ):
2026-07-10 21:44:26 -07:00
_run_cmd (
[ str ( venv_python ), "-m" , "pip" , "install" , "--no-cache-dir" , "playwright" ],
context = "Installing Playwright package" ,
)
2025-12-25 05:10:39 -08:00
try :
cmd = _build_playwright_install_cmd ( args . browsers )
2026-01-12 13:51:26 -08:00
cmd [ 0 ] = str ( venv_python )
2026-07-10 21:44:26 -07:00
_run_cmd ( cmd , context = "Installing Playwright browsers" )
2026-01-12 13:51:26 -08:00
except Exception as exc :
2025-12-25 05:10:39 -08:00
print ( f "Error: { exc } " , file = sys . stderr )
return 2
return 0
2026-01-12 13:51:26 -08:00
# Progress tracking continues for full install
2025-12-25 05:10:39 -08:00
if args . upgrade_pip :
2026-01-12 13:51:26 -08:00
pb . update ( "Upgrading pip/setuptools/wheel..." )
_run_cmd (
2025-12-29 17:05:03 -08:00
[
str ( venv_python ),
"-m" ,
"pip" ,
"install" ,
"--upgrade" ,
2026-01-09 16:02:49 -08:00
"--no-cache-dir" ,
2025-12-29 17:05:03 -08:00
"pip" ,
"setuptools" ,
"wheel" ,
2026-07-10 21:44:26 -07:00
],
context = "Upgrading pip, setuptools, and wheel" ,
2025-12-29 17:05:03 -08:00
)
2025-12-25 05:10:39 -08:00
2026-01-12 13:51:26 -08:00
# 4. Core Dependencies
req_file = repo_root / "scripts" / "requirements.txt"
2026-04-01 13:58:06 -07:00
if should_install_deps :
pb . update ( "Installing core dependencies..." )
if req_file . exists ():
2026-07-10 21:44:26 -07:00
_run_cmd (
[ str ( venv_python ), "-m" , "pip" , "install" , "--no-cache-dir" , "-r" , str ( req_file )],
context = f "Installing dependency set from { req_file } " ,
)
2025-12-25 05:10:39 -08:00
2026-01-12 13:51:26 -08:00
# 5. Playwright Setup
2026-04-01 13:58:06 -07:00
if should_install_playwright :
2026-01-12 13:51:26 -08:00
pb . update ( "Setting up Playwright and browsers..." )
2026-04-01 13:58:06 -07:00
if not playwright_package_installed ( venv_python ):
2026-07-10 21:44:26 -07:00
_run_cmd (
[ str ( venv_python ), "-m" , "pip" , "install" , "--no-cache-dir" , "playwright" ],
context = "Installing Playwright package" ,
)
2025-12-25 05:10:39 -08:00
try :
cmd = _build_playwright_install_cmd ( args . browsers )
2026-01-12 13:51:26 -08:00
cmd [ 0 ] = str ( venv_python )
2026-07-10 21:44:26 -07:00
_run_cmd ( cmd , context = "Installing Playwright browsers" )
2026-01-12 13:51:26 -08:00
except Exception :
pass
2025-12-25 05:10:39 -08:00
2026-01-12 13:51:26 -08:00
# 6. Internal Components
2026-04-01 13:58:06 -07:00
if should_install_project :
pb . update ( "Installing internal components..." )
if platform . system () != "Windows" :
old_mm = venv_dir / "bin" / "mm"
if old_mm . exists ():
try :
old_mm . unlink ()
except Exception :
pass
project_cmd = [ str ( venv_python ), "-m" , "pip" , "install" , "--no-cache-dir" ]
if args . install_editable :
project_cmd . extend ([ "-e" , str ( repo_root / "scripts" )])
else :
project_cmd . append ( str ( repo_root / "scripts" ))
2026-07-10 21:44:26 -07:00
_run_cmd ( project_cmd , context = "Installing Medios-Macina package" )
2025-12-25 05:10:39 -08:00
2026-01-12 13:51:26 -08:00
# 7. CLI Verification
pb . update ( "Verifying CLI configuration..." )
2026-01-31 20:24:15 -08:00
# Check core imports (skip optional python-mpv here because it depends on
# the system libmpv shared library and is not required for the repo-local
# MPV IPC integration; MPV executable availability is handled below.)
2026-01-21 23:37:47 -08:00
try :
missing = []
2026-01-31 20:24:15 -08:00
for mod in [ "importlib" , "shutil" , "subprocess" ]:
2026-01-21 23:37:47 -08:00
try :
2026-01-22 00:22:42 -08:00
# Use run() for verification to ensure clean environment
run ([ str ( venv_python ), "-c" , f "import { mod } " ], quiet = True )
2026-01-21 23:37:47 -08:00
except Exception :
missing . append ( mod )
2026-01-31 20:24:15 -08:00
# Note: If the MPV executable is missing it will be handled by the
# MPV installation step later in this script; do not attempt to
# install the third-party `python-mpv` package here.
2026-01-21 23:37:47 -08:00
if missing :
print ( f " \n Warning: The following packages were not importable in the venv: { ', ' . join ( missing ) } " )
except Exception :
pass
2025-12-25 05:10:39 -08:00
try :
2026-01-22 00:22:42 -08:00
# 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 )
except Exception :
try :
# If direct import fails, try to diagnose why or add a .pth link
2025-12-25 05:10:39 -08:00
cmd = [
str ( venv_python ),
"-c" ,
(
"import site, sysconfig \n "
"out=[] \n "
"try: \n out.extend(site.getsitepackages()) \n except Exception: \n pass \n "
"try: \n p = sysconfig.get_paths().get('purelib') \n if p: \n out.append(p) \n except Exception: \n pass \n "
"seen=[]; res=[] \n "
"for x in out: \n if x and x not in seen: \n seen.append(x); res.append(x) \n "
"for s in res: \n print(s) \n "
),
]
2026-01-22 00:22:42 -08:00
out = subprocess . check_output ( cmd , text = True , env = { ** os . environ , "PYTHONNOUSERSITE" : "1" }) . strip () . splitlines ()
2025-12-25 05:10:39 -08:00
site_dir : Path | None = None
for sp in out :
if sp and Path ( sp ) . exists ():
site_dir = Path ( sp )
break
2026-01-12 13:51:26 -08:00
if site_dir :
2025-12-25 05:10:39 -08:00
pth_file = site_dir / "medeia_repo.pth"
2026-01-12 13:51:26 -08:00
content = str ( repo_root ) + " \n "
2025-12-25 05:10:39 -08:00
if pth_file . exists ():
txt = pth_file . read_text ( encoding = "utf-8" )
2026-01-12 13:51:26 -08:00
if str ( repo_root ) not in txt :
2025-12-25 05:10:39 -08:00
with pth_file . open ( "a" , encoding = "utf-8" ) as fh :
2026-01-12 13:51:26 -08:00
fh . write ( content )
2025-12-25 05:10:39 -08:00
else :
with pth_file . open ( "w" , encoding = "utf-8" ) as fh :
2026-01-12 13:51:26 -08:00
fh . write ( content )
2026-01-22 00:22:42 -08:00
except Exception :
pass
2026-01-12 13:51:26 -08:00
except Exception :
pass
2026-01-10 17:30:18 -08:00
2026-01-12 13:51:26 -08:00
# 8. MPV
install_mpv_requested = not getattr ( args , "no_mpv" , False )
2026-01-10 17:30:18 -08:00
if install_mpv_requested :
2026-01-12 13:51:26 -08:00
pb . update ( "Setting up MPV media player..." )
if not _check_mpv_installed ():
_install_mpv ()
2025-12-25 05:10:39 -08:00
2026-01-12 13:51:26 -08:00
# 9. Deno
install_deno_requested = not getattr ( args , "no_deno" , False )
2025-12-25 05:10:39 -08:00
if install_deno_requested :
2026-01-12 13:51:26 -08:00
pb . update ( "Setting up Deno runtime..." )
if not _check_deno_installed ():
_install_deno ( args . deno_version )
2025-12-25 05:10:39 -08:00
2026-04-29 17:15:56 -07:00
hydrus_web_gui_root = (
Path ( os . path . expandvars ( os . path . expanduser ( args . hydrus_web_gui_root ))) . resolve ()
if args . hydrus_web_gui_root
else repo_root
)
if args . install_hydrus_web_gui :
pb . update ( "Installing Hydrus web GUI..." )
ok = _run_hydrus_web_gui_installer (
hydrus_web_gui_root ,
args . hydrus_web_gui_dest_name ,
install_service = args . install_hydrus_web_gui_service ,
setup_mpv_handler = args . setup_hydrus_web_gui_mpv_handler ,
)
if not ok :
return 1
2026-01-12 13:51:26 -08:00
# 10. Finalizing setup
pb . update ( "Writing launcher scripts..." )
2025-12-25 05:10:39 -08:00
def _write_launchers () -> None :
2025-12-31 22:05:25 -08:00
launcher_dir = repo_root / "scripts"
launcher_dir . mkdir ( parents = True , exist_ok = True )
ps1 = launcher_dir / "mm.ps1"
2025-12-25 05:10:39 -08:00
ps1_text = r """Param([Parameter(ValueFromRemainingArguments=$true)] $args)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
2025-12-31 22:05:25 -08:00
$repo = (Resolve-Path (Join-Path $scriptDir "..")).Path
2026-02-27 22:14:14 -08:00
$venv = Join-Path $repo '.venv'
$py = Join-Path $venv 'Scripts\python.exe'
$requirements = Join-Path $repo 'scripts\requirements.txt'
2026-01-11 10:59:50 -08:00
# Automatically check for updates if this is a git repository
if (Test-Path (Join-Path $repo ".git")) {
try {
if (-not $env:MM_NO_UPDATE) {
$conf = Join-Path $repo "config.conf"
$skip = $false
if (Test-Path $conf) {
2026-02-28 02:45:14 -08:00
if ($null -ne (Get-Content $conf | Select-String "auto_update\s*=\s*(false|no|off|0)")) {
2026-01-11 10:59:50 -08:00
$skip = $true
}
}
if (-not $skip) {
Write-Host "Checking for updates..." -ForegroundColor Gray
2026-02-28 02:45:14 -08:00
Write-Host "[mm] Checking repository updates..." -ForegroundColor DarkGray
2026-01-11 11:30:27 -08:00
$update = git -C "$repo" pull --ff-only 2>&1
2026-02-28 02:45:14 -08:00
$gitExit = $LASTEXITCODE
$updateText = ($update | Out-String)
$repoUpdated = ($updateText -match "Updating|Fast-forward")
$repoUpToDate = ($updateText -match "Already up[\s-]+to[\s-]+date")
2026-06-28 22:10:55 -07:00
$skipDepUpdate = $false
2026-02-28 02:45:14 -08:00
if ($gitExit -ne 0) {
2026-06-28 22:10:55 -07:00
$skipDepUpdate = $true
2026-02-28 02:45:14 -08:00
Write-Host "[mm] Warning: git update check failed; continuing startup." -ForegroundColor Yellow
$gitTail = @($update | Select-Object -Last 3)
foreach ($line in $gitTail) {
if ($line) { Write-Host "[git] $line" -ForegroundColor DarkYellow }
}
} elseif ($repoUpdated) {
Write-Host "[mm] Repository update found (fast-forward/applied)." -ForegroundColor Green
} elseif ($repoUpToDate) {
Write-Host "[mm] Repository already up to date." -ForegroundColor DarkGray
} else {
Write-Host "[mm] Repository update check completed." -ForegroundColor DarkGray
}
2026-02-27 22:14:14 -08:00
2026-06-28 22:10:55 -07:00
if ($skipDepUpdate) {
Write-Host "[mm] Python module update skipped (network/update check unavailable)." -ForegroundColor DarkGray
} elseif (-not $env:MM_NO_DEP_UPDATE -and (Test-Path $py) -and (Test-Path $requirements)) {
2026-02-28 02:45:14 -08:00
Write-Host "[mm] Checking Python module updates..." -ForegroundColor DarkGray
$depOutput = & $py -m pip install --disable-pip-version-check --upgrade -r $requirements 2>&1
$depExit = $LASTEXITCODE
if ($depExit -ne 0) {
Write-Host "[mm] Warning: dependency update failed; continuing startup." -ForegroundColor Yellow
$depTail = @($depOutput | Select-Object -Last 5)
foreach ($line in $depTail) {
if ($line) { Write-Host "[pip] $line" -ForegroundColor DarkYellow }
}
} else {
$depSummary = @($depOutput | Select-String -Pattern "Successfully installed" | Select-Object -Last 1)
if ($depSummary.Count -gt 0) {
Write-Host "[mm] $($depSummary[0].Line)" -ForegroundColor Green
} else {
Write-Host "[mm] Python modules are already up to date." -ForegroundColor DarkGray
}
2026-02-27 22:14:14 -08:00
}
2026-02-28 02:45:14 -08:00
} elseif ($env:MM_NO_DEP_UPDATE) {
Write-Host "[mm] Python module update skipped (MM_NO_DEP_UPDATE=1)." -ForegroundColor DarkGray
} else {
Write-Host "[mm] Python module update skipped (venv python or requirements file missing)." -ForegroundColor DarkGray
2026-02-27 22:14:14 -08:00
}
if ($repoUpdated) {
2026-01-11 11:30:27 -08:00
Write-Host "Medeia-Macina has been updated. Please restart the application to apply changes." -ForegroundColor Cyan
exit 0
}
2026-01-11 10:59:50 -08:00
}
}
} catch {}
}
2026-01-09 13:41:18 -08:00
if (Test-Path $py) {
& $py -m scripts.cli_entry @args; exit $LASTEXITCODE
}
2025-12-25 05:10:39 -08:00
# Ensure venv Scripts dir is on PATH for provider discovery
$venvScripts = Join-Path $venv 'Scripts'
if (Test-Path $venvScripts) { $env:PATH = $venvScripts + ';' + $env:PATH }
2026-01-09 13:41:18 -08:00
# Fallback to system python if venv doesn't exist
if (Test-Path (Join-Path $repo 'CLI.py')) {
python -m scripts.cli_entry @args
} else {
python -m scripts.cli_entry @args
}
2025-12-25 05:10:39 -08:00
"""
try :
ps1 . write_text ( ps1_text , encoding = "utf-8" )
except Exception :
pass
_write_launchers ()
2026-01-12 13:51:26 -08:00
# 11. Global Environment
pb . update ( "Configuring global environment..." )
2025-12-25 05:10:39 -08:00
def _install_user_shims ( repo : Path ) -> None :
try :
home = Path . home ()
system = platform . system () . lower ()
if system == "windows" :
user_bin = Path ( os . environ . get ( "USERPROFILE" , str ( home ))) / "bin"
user_bin . mkdir ( parents = True , exist_ok = True )
2026-01-09 16:36:56 -08:00
# Validate repo path
if not ( repo / ".venv" ) . exists ():
print ( f "WARNING: venv not found at { repo } /.venv - mm command may not work" , file = sys . stderr )
if not ( repo / "scripts" ) . exists ():
print ( f "WARNING: scripts folder not found at { repo } /scripts - mm command may not work" , file = sys . stderr )
2026-07-10 21:49:57 -07:00
# Keep the batch shim deliberately small: cmd.exe parses every
# parenthesized block before executing it, which makes richer
# update logic brittle when paths or messages contain special
# batch characters.
2026-01-10 15:04:16 -08:00
mm_bat = user_bin / "mm.bat"
repo_bat_str = str ( repo )
bat_text = (
"@echo off \n "
2026-07-10 21:49:57 -07:00
"setlocal \n "
2026-01-10 15:04:16 -08:00
f 'set "REPO= { repo_bat_str } " \n '
2026-07-10 21:49:57 -07:00
"set \" PY=%REPO% \\ .venv \\ Scripts \\ python.exe \"\n "
2026-01-11 10:59:50 -08:00
" \n "
2026-07-10 21:49:57 -07:00
"cd /d \" %REPO% \"\n "
"if exist \" %PY% \" ( \n "
" \" %PY% \" -m scripts.cli_entry %* \n "
" exit /b %E RRORLEVEL% \n "
2026-01-11 10:59:50 -08:00
") \n "
2026-07-10 21:49:57 -07:00
"python -m scripts.cli_entry %* \n "
"exit /b %E RRORLEVEL% \n "
2026-01-10 15:04:16 -08:00
)
if mm_bat . exists ():
bak = mm_bat . with_suffix ( f ".bak { int ( time . time ()) } " )
mm_bat . replace ( bak )
mm_bat . write_text ( bat_text , encoding = "utf-8" )
2026-01-09 13:41:18 -08:00
2026-01-10 15:04:16 -08:00
# Validate that the batch shim was created correctly
bat_ok = mm_bat . exists () and len ( mm_bat . read_text ( encoding = "utf-8" )) > 0
if not bat_ok :
raise RuntimeError ( "Failed to create mm.bat shim" )
2026-01-09 16:36:56 -08:00
if args . debug :
2026-04-26 15:08:35 -07:00
print ( f "[bootstrap] Created mm.bat ( { len ( bat_text ) } bytes)" )
print ( f "[bootstrap] Repo path embedded in shim: { repo } " )
print ( f "[bootstrap] Venv location: { repo } /.venv" )
print ( f "[bootstrap] Shim directory: { user_bin } " )
2026-01-09 16:36:56 -08:00
2026-01-09 13:41:18 -08:00
# Add user_bin to PATH for current and future sessions
str_bin = str ( user_bin )
cur_path = os . environ . get ( "PATH" , "" )
# Update current session PATH if not already present
if str_bin not in cur_path :
os . environ [ "PATH" ] = str_bin + ";" + cur_path
# Persist to user's Windows registry PATH for future sessions
2025-12-25 05:10:39 -08:00
try :
2026-01-09 13:41:18 -08:00
ps_cmd = (
"$bin = ' {bin} ';"
"$cur = [Environment]::GetEnvironmentVariable('PATH','User');"
"if ($cur -notlike \" *$bin* \" ) {{"
2026-01-09 17:19:32 -08:00
" $val = if ($cur) {{ $bin + ';' + $cur }} else {{ $bin }};"
" [Environment]::SetEnvironmentVariable('PATH', $val, 'User');"
2026-01-09 13:41:18 -08:00
"}}"
) . format ( bin = str_bin . replace ( " \\ " , " \\\\ " ))
2026-01-09 16:36:56 -08:00
result = subprocess . run (
2026-01-09 13:41:18 -08:00
[ "powershell" ,
"-NoProfile" ,
"-Command" ,
ps_cmd ],
check = False ,
2026-01-09 16:36:56 -08:00
capture_output = True ,
text = True
)
if args . debug and result . stderr :
2026-04-26 15:08:35 -07:00
print ( f "[bootstrap] PowerShell output: { result . stderr } " )
2026-01-09 16:36:56 -08:00
# Also reload PATH in current session for immediate availability
reload_cmd = (
"$env:PATH = [Environment]::GetEnvironmentVariable('PATH', 'User') + ';' + [Environment]::GetEnvironmentVariable('PATH', 'Machine')"
)
subprocess . run (
[ "powershell" ,
"-NoProfile" ,
"-Command" ,
reload_cmd ],
check = False ,
capture_output = True ,
text = True
2026-01-09 13:41:18 -08:00
)
except Exception as e :
2026-01-09 16:36:56 -08:00
if args . debug :
2026-04-26 15:08:35 -07:00
print ( f "[bootstrap] Could not persist PATH to registry: { e } " , file = sys . stderr )
2026-01-09 16:36:56 -08:00
if not args . quiet :
2026-01-09 17:06:48 -08:00
print ( f "Installed global launcher to: { user_bin } " )
2026-01-19 03:14:30 -08:00
print ( "✓ mm.bat (Command Prompt and PowerShell)" )
2026-01-09 16:36:56 -08:00
print ()
2026-01-09 17:06:48 -08:00
print ( "You can now run 'mm' from any terminal window." )
2026-01-19 03:14:30 -08:00
print ( "If 'mm' is not found, restart your terminal or reload PATH:" )
2026-01-09 17:06:48 -08:00
print ( " PowerShell: $env:PATH = [Environment]::GetEnvironmentVariable('PATH','User') + ';' + [Environment]::GetEnvironmentVariable('PATH','Machine')" )
print ( " CMD: path %PATH%" )
2025-12-25 05:10:39 -08:00
else :
# POSIX
2026-01-10 23:20:00 -08:00
# If running as root (id 0), prefer /usr/bin or /usr/local/bin which are standard on PATH
2026-01-22 04:08:07 -08:00
is_root = False
try :
if platform . system () . lower () != "windows" and os . getuid () == 0 :
is_root = True
except ( AttributeError , Exception ):
pass
if is_root :
2026-01-10 23:20:00 -08:00
user_bin = Path ( "/usr/local/bin" )
if not os . access ( user_bin , os . W_OK ):
user_bin = Path ( "/usr/bin" )
else :
user_bin = Path ( os . environ . get ( "XDG_BIN_HOME" , str ( home / ".local/bin" )))
2025-12-25 05:10:39 -08:00
user_bin . mkdir ( parents = True , exist_ok = True )
mm_sh = user_bin / "mm"
2026-01-10 22:28:11 -08:00
2026-01-10 23:20:00 -08:00
# Search PATH/standard locations for existing 'mm' shims to avoid conflicts
common_paths = [ user_bin , Path ( "/usr/local/bin" ), Path ( "/usr/bin" ), home / ".local" / "bin" ]
for p_dir in common_paths :
p_mm = p_dir / "mm"
if p_mm . exists () and p_mm . resolve () != mm_sh . resolve ():
try :
# Only remove if it looks like one of our shims
content = p_mm . read_text ( encoding = "utf-8" , errors = "ignore" )
if "Medeia" in content or "Medios" in content or "cli_entry" in content :
p_mm . unlink ()
if not args . quiet :
print ( f "Removed conflicting old shim: { p_mm } " )
except Exception :
pass
2026-01-10 22:28:11 -08:00
# Remove old launcher to overwrite with new one
if mm_sh . exists ():
try :
mm_sh . unlink ()
except Exception :
pass
2025-12-25 05:10:39 -08:00
sh_text = (
"#!/usr/bin/env bash \n "
"set -e \n "
2025-12-29 17:05:03 -08:00
f 'REPO=" { repo } " \n '
2025-12-25 05:10:39 -08:00
"# Prefer git top-level when available to avoid embedding a parent path. \n "
"if command -v git >/dev/null 2>&1; then \n "
2025-12-29 17:05:03 -08:00
' gitroot=$(git -C "$REPO" rev-parse --show-toplevel 2>/dev/null || true) \n '
' if [ -n "$gitroot" ]; then \n '
' REPO="$gitroot" \n '
2025-12-25 05:10:39 -08:00
" fi \n "
"fi \n "
"# If git not available or didn't resolve, walk up from CWD to find a project root. \n "
2025-12-31 22:58:54 -08:00
'if [ ! -f "$REPO/CLI.py" ] && [ ! -f "$REPO/pyproject.toml" ] && [ ! -f "$REPO/scripts/pyproject.toml" ]; then \n '
2025-12-29 17:05:03 -08:00
' CUR="$(pwd -P)" \n '
' while [ "$CUR" != "/" ] && [ "$CUR" != "" ]; do \n '
2025-12-31 22:58:54 -08:00
' if [ -f "$CUR/CLI.py" ] || [ -f "$CUR/pyproject.toml" ] || [ -f "$CUR/scripts/pyproject.toml" ]; then \n '
2025-12-29 17:05:03 -08:00
' REPO="$CUR" \n '
2025-12-25 05:10:39 -08:00
" break \n "
" fi \n "
2025-12-29 17:05:03 -08:00
' CUR="$(dirname "$CUR")" \n '
2025-12-25 05:10:39 -08:00
" done \n "
"fi \n "
2025-12-29 17:05:03 -08:00
'VENV="$REPO/.venv" \n '
2025-12-25 05:10:39 -08:00
"# Debug mode: set MM_DEBUG=1 to print repository, venv, and import diagnostics \n "
2025-12-29 17:05:03 -08:00
'if [ -n "$ {MM_DEBUG:-} " ]; then \n '
2026-04-26 15:08:35 -07:00
' echo "[mm-debug] diagnostics" >&2 \n '
2025-12-29 17:05:03 -08:00
' echo "Resolved REPO: $REPO" >&2 \n '
' echo "Resolved VENV: $VENV" >&2 \n '
' echo "VENV exists: $( [ -d "$VENV" ] && echo yes || echo no )" >&2 \n '
' echo "Candidates:" >&2 \n '
' echo " VENV/bin/mm: $( [ -x "$VENV/bin/mm" ] && echo yes || echo no )" >&2 \n '
' echo " VENV/bin/python3: $( [ -x "$VENV/bin/python3" ] && echo yes || echo no )" >&2 \n '
' echo " VENV/bin/python: $( [ -x "$VENV/bin/python" ] && echo yes || echo no )" >&2 \n '
' echo " system python3: $(command -v python3 || echo none)" >&2 \n '
' echo " system python: $(command -v python || echo none)" >&2 \n '
' for pycmd in "$VENV/bin/python3" "$VENV/bin/python" "$(command -v python3 2>/dev/null)" "$(command -v python 2>/dev/null)"; do \n '
' if [ -n "$pycmd" ] && [ -x "$pycmd" ]; then \n '
' echo "---- Testing with: $pycmd ----" >&2 \n '
2026-01-01 00:54:03 -08:00
" $pycmd - <<'PY' \n import sys, importlib, traceback, importlib.util \n print('sys.executable:', sys.executable) \n print('sys.path (first 8):', sys.path[:8]) \n for mod in ('CLI','medeia_macina','scripts.cli_entry'): \n try: \n spec = importlib.util.find_spec(mod) \n print(mod, 'spec:', spec) \n if spec: \n m = importlib.import_module(mod) \n print(mod, 'loaded at', getattr(m, '__file__', None)) \n except Exception: \n print(mod, 'import failed') \n traceback.print_exc() \n PY \n "
2025-12-25 05:10:39 -08:00
" fi \n "
" done \n "
2026-04-26 15:08:35 -07:00
' echo "[mm-debug] end diagnostics" >&2 \n '
2025-12-25 05:10:39 -08:00
"fi \n "
2026-01-11 10:59:50 -08:00
" \n "
"# Automatically check for updates if this is a git repository \n "
'if [ -z "$ {MM_NO_UPDATE:-} " ] && [ -d "$REPO/.git" ] && command -v git >/dev/null 2>&1; then \n '
' AUTO_UPDATE="true" \n '
' if [ -f "$REPO/config.conf" ]; then \n '
2026-01-11 11:16:54 -08:00
" if grep -qiE 'auto_update[[:space:]]*=[[:space:]]*(false|no|off|0)' \" $REPO/config.conf \" ; then \n "
2026-01-11 10:59:50 -08:00
' AUTO_UPDATE="false" \n '
' fi \n '
' fi \n '
' if [ "$AUTO_UPDATE" = "true" ]; then \n '
' echo "Checking for updates..." \n '
2026-02-28 02:45:14 -08:00
' echo "[mm] Checking repository updates..." \n '
2026-01-11 11:30:27 -08:00
' UPDATE_OUT=$(git -C "$REPO" pull --ff-only 2>&1) \n '
2026-02-28 02:45:14 -08:00
' UPDATE_EXIT=$? \n '
2026-02-27 22:14:14 -08:00
' REPO_UPDATED="false" \n '
2026-06-28 22:10:55 -07:00
' SKIP_DEP_UPDATE="false" \n '
2026-02-28 02:45:14 -08:00
' if [ $UPDATE_EXIT -ne 0 ]; then \n '
2026-06-28 22:10:55 -07:00
' SKIP_DEP_UPDATE="true" \n '
2026-02-28 02:45:14 -08:00
' echo "[mm] Warning: git update check failed; continuing startup." \n '
' printf " %s \n " "$UPDATE_OUT" | tail -n 3 \n '
' elif echo "$UPDATE_OUT" | grep -qiE \' Updating|Fast-forward \' ; then \n '
2026-02-27 22:14:14 -08:00
' REPO_UPDATED="true" \n '
2026-02-28 02:45:14 -08:00
' echo "[mm] Repository update found (fast-forward/applied)." \n '
' elif echo "$UPDATE_OUT" | grep -qiE \' Already up[ -]to[ -]date \' ; then \n '
' echo "[mm] Repository already up to date." \n '
' else \n '
' echo "[mm] Repository update check completed." \n '
2026-02-27 22:14:14 -08:00
' fi \n '
' MM_PY="" \n '
' if [ -x "$VENV/bin/python3" ]; then \n '
' MM_PY="$VENV/bin/python3" \n '
' elif [ -x "$VENV/bin/python" ]; then \n '
' MM_PY="$VENV/bin/python" \n '
' fi \n '
2026-06-28 22:10:55 -07:00
' if [ "$SKIP_DEP_UPDATE" = "true" ]; then \n '
' echo "[mm] Python module update skipped (network/update check unavailable)." \n '
' elif [ -z "$ {MM_NO_DEP_UPDATE:-} " ] && [ -n "$MM_PY" ] && [ -f "$REPO/scripts/requirements.txt" ]; then \n '
2026-02-28 02:45:14 -08:00
' echo "[mm] Checking Python module updates..." \n '
' DEP_OUT=$("$MM_PY" -m pip install --disable-pip-version-check --upgrade -r "$REPO/scripts/requirements.txt" 2>&1) \n '
' DEP_EXIT=$? \n '
' if [ $DEP_EXIT -ne 0 ]; then \n '
' echo "[mm] Warning: dependency update failed; continuing startup." \n '
' printf " %s \n " "$DEP_OUT" | tail -n 5 \n '
' else \n '
' DEP_SUM=$(printf " %s \n " "$DEP_OUT" | grep -i "Successfully installed" | tail -n 1 || true) \n '
' if [ -n "$DEP_SUM" ]; then \n '
' echo "[mm] $DEP_SUM" \n '
' else \n '
' echo "[mm] Python modules are already up to date." \n '
' fi \n '
' fi \n '
' elif [ -n "$ {MM_NO_DEP_UPDATE:-} " ]; then \n '
' echo "[mm] Python module update skipped (MM_NO_DEP_UPDATE=1)." \n '
' else \n '
' echo "[mm] Python module update skipped (venv python or requirements file missing)." \n '
2026-02-27 22:14:14 -08:00
' fi \n '
' if [ "$REPO_UPDATED" = "true" ]; then \n '
2026-01-11 11:30:27 -08:00
' echo "Medeia-Macina has been updated. Please restart the application to apply changes." \n '
' exit 0 \n '
' fi \n '
2026-01-11 10:59:50 -08:00
' fi \n '
"fi \n "
" \n "
2026-01-10 22:22:26 -08:00
"# Use -m scripts.cli_entry directly instead of pip-generated wrapper to avoid entry point issues \n "
2025-12-25 05:10:39 -08:00
"# Prefer venv's python3, then venv's python \n "
2025-12-29 17:05:03 -08:00
'if [ -x "$VENV/bin/python3" ]; then \n '
2026-01-01 00:54:03 -08:00
' exec "$VENV/bin/python3" -m scripts.cli_entry "$@" \n '
2025-12-25 05:10:39 -08:00
"fi \n "
2025-12-29 17:05:03 -08:00
'if [ -x "$VENV/bin/python" ]; then \n '
2026-01-01 00:54:03 -08:00
' exec "$VENV/bin/python" -m scripts.cli_entry "$@" \n '
2025-12-25 05:10:39 -08:00
"fi \n "
"# Fallback to system python3, then system python (only if it's Python 3) \n "
"if command -v python3 >/dev/null 2>&1; then \n "
2026-01-01 00:54:03 -08:00
' exec python3 -m scripts.cli_entry "$@" \n '
2025-12-25 05:10:39 -08:00
"fi \n "
"if command -v python >/dev/null 2>&1; then \n "
" if python -c 'import sys; sys.exit(0 if sys.version_info[0] >= 3 else 1)'; then \n "
2026-01-01 00:54:03 -08:00
' exec python -m scripts.cli_entry "$@" \n '
2025-12-25 05:10:39 -08:00
" fi \n "
"fi \n "
"echo 'Error: no suitable Python 3 interpreter found. Please install Python 3 or use the venv.' >&2 \n "
"exit 127 \n "
)
if mm_sh . exists ():
bak = mm_sh . with_suffix ( f ".bak { int ( time . time ()) } " )
mm_sh . replace ( bak )
mm_sh . write_text ( sh_text , encoding = "utf-8" )
mm_sh . chmod ( mm_sh . stat () . st_mode | 0o111 )
# Ensure the user's bin is on PATH for future sessions by adding to ~/.profile
cur_path = os . environ . get ( "PATH" , "" )
if str ( user_bin ) not in cur_path :
profile = home / ".profile"
snippet = (
"# Added by Medeia-Macina setup: ensure user local bin is on PATH \n "
2025-12-29 17:05:03 -08:00
'if [ -d "$HOME/.local/bin" ] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then \n '
' PATH="$HOME/.local/bin:$PATH" \n '
2025-12-25 05:10:39 -08:00
"fi \n "
)
try :
txt = profile . read_text () if profile . exists () else ""
if snippet . strip () not in txt :
with profile . open ( "a" , encoding = "utf-8" ) as fh :
fh . write ( " \n " + snippet )
except Exception :
pass
2025-12-31 16:10:35 -08:00
if not args . quiet :
print ( f "Installed global launcher to: { mm_sh } " )
2025-12-25 05:10:39 -08:00
except Exception as exc : # pragma: no cover - best effort
print ( f "Failed to install global shims: { exc } " , file = sys . stderr )
_install_user_shims ( repo_root )
2026-01-09 16:36:56 -08:00
if not args . quiet :
print ()
2026-07-10 21:44:26 -07:00
print ( "Installation completed successfully." )
print ( f "Environment path: { venv_dir } " )
print ( "Launch command from terminal: mm" )
print ( "Inside the app: .config" )
2026-01-09 16:36:56 -08:00
print ()
2025-12-25 05:10:39 -08:00
return 0
except subprocess . CalledProcessError as exc :
2025-12-29 18:42:02 -08:00
print (
f "Error: command failed with exit { exc . returncode } : { exc } " ,
file = sys . stderr
)
2025-12-25 05:10:39 -08:00
return int ( exc . returncode or 1 )
except Exception as exc : # pragma: no cover - defensive
print ( f "Unexpected error: { exc } " , file = sys . stderr )
return 2
if __name__ == "__main__" :
raise SystemExit ( main ())