Files
Medios-Macina/scripts/bootstrap.py

1605 lines
70 KiB
Python
Raw Normal View History

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
(`scripts/bootstrap.ps1` and `scripts/bootstrap.sh`) are now thin wrappers
that delegate to this script (they call it with `--no-delegate -q`).
2025-12-25 05:10:39 -08:00
When invoked without any arguments, `bootstrap.py` will automatically select and
run the platform-specific bootstrap helper (`scripts/bootstrap.ps1` on Windows
or `scripts/bootstrap.sh` on POSIX) in **non-interactive (quiet)** mode so a
single `python ./scripts/bootstrap.py` call does the usual bootstrap on your OS.
The platform bootstrap scripts also attempt (best-effort) to install `mpv` if
it is not found on your PATH, since some workflows use it.
This file replaces the old `scripts/setup.py` to ensure the repository only has
one `setup.py` (at the repository root) for packaging.
Usage:
python ./scripts/bootstrap.py # install deps and playwright browsers (or run platform bootstrap if no args)
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
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":
sys.stdin = open("CONIN$", "r")
else:
sys.stdin = open("/dev/tty", "r")
except Exception:
pass
2026-01-12 13:51:26 -08:00
def run(cmd: list[str], quiet: bool = False, debug: bool = False, cwd: Optional[Path] = None) -> None:
if debug:
print(f"\n> {' '.join(cmd)}")
if quiet and not debug:
subprocess.check_call(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
cwd=str(cwd) if cwd else None
)
else:
if not debug:
print(f"> {' '.join(cmd)}")
subprocess.check_call(cmd, cwd=str(cwd) if cwd else None)
2026-01-21 22:52:52 -08:00
REPO_URL = "https://code.glowers.club/goyimnose/Medios-Macina.git"
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-12 17:55:04 -08:00
term_width = shutil.get_terminal_size((80, 20)).columns
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-01-12 17:55:04 -08:00
# Overwrite previous bar/label by moving up if not the first step
if self.current > 1:
sys.stdout.write("\033[2A")
bar_line = f"[{bar}]"
info_line = f"{percent}% | {step_name}"
sys.stdout.write(f"\r{bar_line.center(term_width)}\n")
# Clear line and print info line centered
sys.stdout.write(f"\r\033[K{info_line.center(term_width)}\r")
2026-01-12 13:51:26 -08:00
sys.stdout.flush()
if self.current == self.total:
sys.stdout.write("\n")
sys.stdout.flush()
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:
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)
def playwright_package_installed() -> bool:
try:
return True
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
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...")
run(["winget", "install", "--id=mpv.net", "-e"])
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"
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)",
2025-12-25 05:10:39 -08:00
)
parser.add_argument(
2025-12-29 17:05:03 -08:00
"--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",
help="Do not delegate to platform bootstrap scripts; run the Python bootstrap directly.",
)
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",
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(
"--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",
)
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-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()
repo_root = Path.cwd()
# DETECT REPOSITORY
# Check if we are already inside a valid Medios-Macina repo
def _is_valid_mm_repo(p: Path) -> bool:
return (p / "CLI.py").exists() and (p / "scripts").exists()
is_in_repo = _is_valid_mm_repo(repo_root)
2026-01-09 15:41:38 -08:00
2026-01-21 22:52:52 -08:00
# If not in the parent of the script, check the current working directory
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
2026-01-09 15:41:38 -08:00
if not args.quiet:
print(f"Bootstrap script location: {script_dir}")
print(f"Detected project root: {repo_root}")
print(f"Current working directory: {Path.cwd()}")
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."""
vdir = repo_root / ".venv"
py = _venv_python_path(vdir)
if py is None:
return False
try:
rc = subprocess.run([str(py), "-m", "pip", "show", "medeia-macina"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False)
return rc.returncode == 0
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.
try:
current_exe = Path(sys.executable).resolve()
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:
config_path = root / "config.conf"
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]\nname="hydrus"\n{key}="{value}"'
config_path.write_text(new_content, encoding="utf-8")
return True
except Exception as e:
print(f"Error updating config: {e}")
return False
2025-12-31 16:10:35 -08:00
def _interactive_menu() -> str | int:
"""Show a simple interactive menu to choose install/uninstall or delegate."""
try:
installed = _is_installed()
2026-01-12 16:15:51 -08:00
term_width = shutil.get_terminal_size((80, 20)).columns
2025-12-31 16:10:35 -08:00
while True:
2026-01-11 12:28:16 -08:00
os.system("cls" if os.name == "nt" else "clear")
2026-01-12 16:15:51 -08:00
# Center the logo
logo_lines = LOGO.strip().splitlines()
print("\n" * 2)
for line in logo_lines:
print(line.center(term_width))
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")
install_text = "1) Reinstall" if installed else "1) Install"
print(install_text.center(term_width))
print("2) Extras > HydrusNetwork".center(term_width))
2025-12-31 16:10:35 -08:00
if installed:
2026-01-12 16:15:51 -08:00
print("3) Uninstall".center(term_width))
print("q) Quit".center(term_width))
2026-01-11 11:11:32 -08:00
2026-01-12 16:15:51 -08:00
prompt = "\nChoose an option: "
# Try to center the prompt roughly
indent = " " * ((term_width // 2) - (len(prompt) // 2))
choice = input(f"{indent}{prompt}").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-01-11 11:11:32 -08:00
if installed and choice in ("3", "uninstall"):
return "uninstall"
if choice in ("q", "quit", "exit"):
return 0
2025-12-31 16:10:35 -08:00
except EOFError:
# Non-interactive, fall back to delegating to platform helper
return "delegate"
# 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-11 11:16:54 -08:00
sel = _interactive_menu()
if sel == "install":
2026-01-21 23:09:12 -08:00
# If running via pipe/standalone or not in a repo, ask for installation path
2026-01-21 23:15:32 -08:00
if script_path is None or not is_in_repo:
2026-01-21 23:06:43 -08:00
if not shutil.which("git"):
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)
return 1
try:
2026-01-21 23:15:32 -08:00
# Force a prompt to choose the folder
2026-01-21 23:09:12 -08:00
if is_in_repo:
default_install = repo_root
else:
default_install = Path.cwd() / "Medios-Macina"
2026-01-21 23:15:32 -08:00
print(f"\n[WEB INSTALLER MODE]")
print(f"Where would you like to install Medios-Macina?")
2026-01-21 23:06:43 -08:00
install_dir_raw = input(f"Installation directory [{default_install}]: ").strip()
if not install_dir_raw:
install_path = default_install
else:
install_path = Path(install_dir_raw).resolve()
2026-01-21 23:15:32 -08:00
except (EOFError, KeyboardInterrupt):
2026-01-21 23:06:43 -08:00
return 1
if not install_path.exists():
print(f"Creating directory: {install_path}")
install_path.mkdir(parents=True, exist_ok=True)
# Check if it already has a repo (user might have chosen an existing folder)
if _is_valid_mm_repo(install_path):
2026-01-21 23:09:12 -08:00
if not args.quiet:
print(f"Using existing repository in {install_path}.")
2026-01-21 23:06:43 -08:00
repo_root = install_path
else:
2026-01-21 23:15:32 -08:00
print(f"Cloning Medios-Macina into {install_path} (depth 1)...")
2026-01-21 23:06:43 -08:00
print(f"Source: {REPO_URL}")
try:
subprocess.check_call(["git", "clone", "--depth", "1", REPO_URL, str(install_path)])
repo_root = install_path
except Exception as e:
print(f"Error: Failed to clone repository: {e}", file=sys.stderr)
return 1
# Change directory to the newly established repo root
os.chdir(str(repo_root))
2026-01-21 23:09:12 -08:00
if not args.quiet:
print(f"\nSuccessfully set up repository at {repo_root}")
print("Resuming bootstrap...\n")
2026-01-21 23:06:43 -08:00
# Re-initialize script_dir for the rest of the script
# as if we started inside the repo scripts folder.
script_dir = repo_root / "scripts"
is_in_repo = True
2026-01-11 11:16:54 -08:00
# user chose to install/reinstall; set defaults and continue
args.skip_deps = False
args.install_editable = True
args.no_playwright = False
elif sel == "extras_hydrus":
# Special case: run the hydrusnetwork.py script and then exit
hydrus_script = repo_root / "scripts" / "hydrusnetwork.py"
if hydrus_script.exists():
try:
subprocess.check_call([sys.executable, str(hydrus_script)])
except subprocess.CalledProcessError:
print("\nHydrusNetwork setup exited with an error.")
except Exception as e:
print(f"\nFailed to run HydrusNetwork setup: {e}")
2025-12-31 16:10:35 -08:00
else:
2026-01-11 11:16:54 -08:00
print(f"\nError: {hydrus_script} not found.")
return 0
elif sel == "uninstall":
return _do_uninstall()
elif sel == "delegate":
2025-12-31 16:10:35 -08:00
rc = run_platform_bootstrap(repo_root)
if rc != 0:
return rc
if not args.quiet:
print("Platform bootstrap completed successfully.")
return 0
2026-01-11 11:16:54 -08:00
elif sel == 0:
return 0
2026-01-21 23:15:32 -08:00
elif not args.no_delegate and script_path is not None:
2026-01-11 11:16:54 -08:00
# Default non-interactive behavior: delegate to platform script
rc = run_platform_bootstrap(repo_root)
if rc != 0:
return rc
if not args.quiet:
print("Platform bootstrap completed successfully.")
return 0
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
# Determine total steps for progress bar
total_steps = 7 # Base: venv, pip, deps, project, cli, finalize, env
if args.upgrade_pip: total_steps += 1
if not args.no_playwright: total_steps += 1 # Playwright is combined pkg+browsers
if not getattr(args, "no_mpv", False): total_steps += 1
if not getattr(args, "no_deno", False): total_steps += 1
pb = ProgressBar(total_steps, quiet=args.quiet or args.debug)
def _run_cmd(cmd: list[str], cwd: Optional[Path] = None):
"""Helper to run commands with shared settings."""
run(cmd, quiet=not args.debug, debug=args.debug, cwd=cwd)
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:
subprocess.run(
[str(python_path), "-m", "pip", "--version"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=True,
)
return
except Exception:
pass
try:
2026-01-12 13:51:26 -08:00
_run_cmd([str(python_path), "-m", "ensurepip", "--upgrade"])
2026-01-19 03:14:30 -08:00
except subprocess.CalledProcessError:
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
# Enforce opinionated behavior: install deps, playwright, deno, and install project in editable mode.
# Ignore `--skip-deps` and `--install-editable` flags to keep the setup deterministic.
args.skip_deps = False
args.install_editable = True
args.no_playwright = False
try:
if args.playwright_only:
2026-01-12 13:51:26 -08:00
# Playwright browser install (short-circuit)
2025-12-25 05:10:39 -08:00
if not playwright_package_installed():
2026-01-12 13:51:26 -08:00
_run_cmd([sys.executable, "-m", "pip", "install", "--no-cache-dir", "playwright"])
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)
_run_cmd(cmd)
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",
]
)
2025-12-25 05:10:39 -08:00
2026-01-12 13:51:26 -08:00
# 4. Core Dependencies
pb.update("Installing core dependencies...")
req_file = repo_root / "scripts" / "requirements.txt"
if req_file.exists():
_run_cmd([str(venv_python), "-m", "pip", "install", "--no-cache-dir", "-r", str(req_file)])
2025-12-25 05:10:39 -08:00
2026-01-12 13:51:26 -08:00
# 5. Playwright Setup
2025-12-25 05:10:39 -08:00
if not args.no_playwright:
2026-01-12 13:51:26 -08:00
pb.update("Setting up Playwright and browsers...")
2025-12-25 05:10:39 -08:00
if not playwright_package_installed():
2026-01-12 13:51:26 -08:00
_run_cmd([str(venv_python), "-m", "pip", "install", "--no-cache-dir", "playwright"])
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)
_run_cmd(cmd)
except Exception:
pass
2025-12-25 05:10:39 -08:00
2026-01-12 13:51:26 -08:00
# 6. Internal Components
pb.update("Installing internal components...")
2026-01-10 22:28:11 -08:00
if platform.system() != "Windows":
old_mm = venv_dir / "bin" / "mm"
if old_mm.exists():
try:
old_mm.unlink()
except Exception:
pass
2026-01-12 13:51:26 -08:00
_run_cmd([str(venv_python), "-m", "pip", "install", "--no-cache-dir", "-e", str(repo_root / "scripts")])
2025-12-25 05:10:39 -08:00
2026-01-12 13:51:26 -08:00
# 7. CLI Verification
pb.update("Verifying CLI configuration...")
2025-12-25 05:10:39 -08:00
try:
2026-01-19 06:24:09 -08:00
cli_verify_result = subprocess.run(
[
str(venv_python),
"-c",
"import importlib; importlib.import_module('CLI')"
],
2026-01-12 13:51:26 -08:00
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
2025-12-25 05:10:39 -08:00
check=False,
)
2026-01-19 06:24:09 -08:00
if cli_verify_result.returncode != 0:
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())\nexcept Exception:\n pass\n"
"try:\n p = sysconfig.get_paths().get('purelib')\n if p:\n out.append(p)\nexcept 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"
),
]
out = subprocess.check_output(cmd, text=True).strip().splitlines()
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)
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-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-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) {
if ((Get-Content $conf | Select-String "auto_update\s*=\s*(false|no|off|0)") -ne $null) {
$skip = $true
}
}
if (-not $skip) {
Write-Host "Checking for updates..." -ForegroundColor Gray
2026-01-11 11:30:27 -08:00
$update = git -C "$repo" pull --ff-only 2>&1
if ($update -like "*Updating*" -or $update -like "*Fast-forward*") {
Clear-Host
Write-Host "Medeia-Macina has been updated. Please restart the application to apply changes." -ForegroundColor Cyan
exit 0
}
Clear-Host
2026-01-11 10:59:50 -08:00
}
}
} catch {}
}
2025-12-25 05:10:39 -08:00
$venv = Join-Path $repo '.venv'
2026-01-09 13:41:18 -08:00
$py = Join-Path $venv 'Scripts\python.exe'
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-01-10 15:04:16 -08:00
# Write mm.bat (batch shim works in all shells, bypasses PowerShell execution policy)
mm_bat = user_bin / "mm.bat"
repo_bat_str = str(repo)
bat_text = (
"@echo off\n"
"setlocal enabledelayedexpansion\n"
f'set "REPO={repo_bat_str}"\n'
2026-01-11 10:59:50 -08:00
"\n"
2026-01-12 13:51:26 -08:00
":: Automatically check for updates if this is a git repository\n"
2026-01-11 10:59:50 -08:00
"if not defined MM_NO_UPDATE (\n"
" if exist \"!REPO!\\.git\" (\n"
" set \"AUTO_UPDATE=true\"\n"
" if exist \"!REPO!\\config.conf\" (\n"
" findstr /i /r \"auto_update.*=.*false auto_update.*=.*no auto_update.*=.*off auto_update.*=.*0\" \"!REPO!\\config.conf\" >nul 2>&1\n"
" if !errorlevel! == 0 set \"AUTO_UPDATE=false\"\n"
" )\n"
" if \"!AUTO_UPDATE!\" == \"true\" (\n"
" echo Checking for updates...\n"
2026-01-11 11:30:27 -08:00
" git -C \"!REPO!\" pull --ff-only | findstr /i /c:\"Updating\" /c:\"Fast-forward\" >nul 2>&1\n"
" if !errorlevel! == 0 (\n"
" cls\n"
" echo Medeia-Macina has been updated. Please restart the application to apply changes.\n"
" exit /b 0\n"
" )\n"
" cls\n"
2026-01-11 10:59:50 -08:00
" )\n"
" )\n"
")\n"
"\n"
2026-01-10 15:04:16 -08:00
"set \"VENV=!REPO!\\.venv\"\n"
"set \"PY=!VENV!\\Scripts\\python.exe\"\n"
"set \"ENTRY=!REPO!\\scripts\\cli_entry.py\"\n"
"if exist \"!PY!\" (\n"
" \"!PY!\" \"!ENTRY!\" %*\n"
" exit /b !ERRORLEVEL!\n"
")\n"
"python \"!ENTRY!\" %*\n"
"exit /b !ERRORLEVEL!\n"
)
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")
# 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-01-10 15:04:16 -08:00
print(f"DEBUG: Created mm.bat ({len(bat_text)} bytes)")
2026-01-09 22:19:03 -08:00
print(f"DEBUG: Repo path embedded in shim: {repo}")
2026-01-09 16:36:56 -08:00
print(f"DEBUG: Venv location: {repo}/.venv")
print(f"DEBUG: Shim directory: {user_bin}")
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:
print(f"DEBUG: PowerShell output: {result.stderr}")
# 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:
print(f"DEBUG: Could not persist PATH to registry: {e}", file=sys.stderr)
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-19 06:24:09 -08:00
if hasattr(os, "getuid") and os.getuid() == 0:
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'
' echo "MM_DEBUG: diagnostics" >&2\n'
' 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'\nimport sys, importlib, traceback, importlib.util\nprint('sys.executable:', sys.executable)\nprint('sys.path (first 8):', sys.path[:8])\nfor 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()\nPY\n"
2025-12-25 05:10:39 -08:00
" fi\n"
" done\n"
2025-12-29 17:05:03 -08: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-01-11 11:30:27 -08:00
' UPDATE_OUT=$(git -C "$REPO" pull --ff-only 2>&1)\n'
' if echo "$UPDATE_OUT" | grep -qiE \'Updating|Fast-forward\'; then\n'
' clear\n'
' echo "Medeia-Macina has been updated. Please restart the application to apply changes."\n'
' exit 0\n'
' fi\n'
' clear\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:
2026-01-21 20:35:19 -08:00
os.system('cls' if os.name == 'nt' else 'clear')
2026-01-09 16:36:56 -08:00
print()
2026-01-12 16:26:30 -08:00
print("command: mm")
print(".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:
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())