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 ) .
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 )
2025-12-31 22:58:54 -08:00
- - install - editable Install the project in editable mode ( pip install - e scripts ) for running tests
2025-12-25 05:10:39 -08:00
- - install - deno Install the Deno runtime using the official installer
- - 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
"""
from __future__ import annotations
import argparse
import os
import platform
from pathlib import Path
import shutil
import subprocess
import sys
import time
def run ( cmd : list [ str ] ) - > None :
print ( f " > { ' ' . join ( cmd ) } " )
subprocess . check_call ( cmd )
# 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 )
def playwright_package_installed ( ) - > bool :
try :
import playwright # type: ignore
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
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
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 :
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 " ,
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 " ,
2025-12-31 22:58:54 -08:00
help = " Install the project in editable mode (pip install -e scripts) for running tests " ,
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
)
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
script_dir = Path ( __file__ ) . resolve ( ) . parent
repo_root = script_dir . parent
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
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 ( )
while True :
print ( " \n Medeia-Macina bootstrap - interactive menu " )
if installed :
print ( " 1) Install / Reinstall " )
print ( " 2) Uninstall " )
print ( " 3) Status " )
print ( " q) Quit " )
choice = input ( " Choose an option: " ) . strip ( ) . lower ( )
if not choice or choice in ( " 1 " , " install " , " reinstall " ) :
return " install "
if choice in ( " 2 " , " uninstall " ) :
return " uninstall "
if choice in ( " 3 " , " status " ) :
print ( " Installation detected. " if installed else " Not installed. " )
continue
if choice in ( " q " , " quit " , " exit " ) :
return 0
else :
print ( " 1) Install " )
print ( " q) Quit " )
choice = input ( " Choose an option: " ) . strip ( ) . lower ( )
if not choice or choice in ( " 1 " , " install " ) :
return " install "
if choice in ( " q " , " quit " , " exit " ) :
return 0
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 ( )
# If invoked without any arguments and not asked to skip delegation, prefer
# the interactive menu when running in a TTY; otherwise delegate to the
# platform-specific bootstrap helper (non-interactive).
if len ( sys . argv ) == 1 and not args . no_delegate :
if sys . stdin . isatty ( ) and not args . quiet :
sel = _interactive_menu ( )
if sel == " install " :
# user chose to install/reinstall; set defaults and continue
args . skip_deps = False
args . install_editable = True
args . no_playwright = False
elif sel == " uninstall " :
return _do_uninstall ( )
elif sel == " delegate " :
rc = run_platform_bootstrap ( repo_root )
if rc != 0 :
return rc
if not args . quiet :
print ( " Platform bootstrap completed successfully. " )
return 0
else :
return int ( sel or 0 )
else :
rc = run_platform_bootstrap ( repo_root )
if rc != 0 :
return rc
if not args . quiet :
print ( " Platform bootstrap completed successfully. " )
return 0
2025-12-25 05:10:39 -08:00
if sys . version_info < ( 3 , 8 ) :
print ( " Warning: Python 3.8+ is recommended. " , file = sys . stderr )
# 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
def _venv_python ( p : Path ) - > Path :
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 ( ) :
2025-12-31 16:10:35 -08:00
if not args . quiet :
print ( f " Creating local virtualenv at: { venv_dir } " )
2025-12-25 05:10:39 -08:00
run ( [ sys . executable , " -m " , " venv " , str ( venv_dir ) ] )
else :
2025-12-31 16:10:35 -08:00
if not args . quiet :
print ( f " Using existing virtualenv at: { venv_dir } " )
2025-12-25 05:10:39 -08:00
py = _venv_python ( venv_dir )
if not py . exists ( ) :
# Try recreating venv if python is missing
2025-12-31 16:10:35 -08:00
if not args . quiet :
print ( f " Local venv python not found at { py } ; recreating venv " )
2025-12-25 05:10:39 -08:00
run ( [ sys . executable , " -m " , " venv " , str ( venv_dir ) ] )
py = _venv_python ( venv_dir )
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
if not args . quiet :
print ( " Bootstrapping pip inside the local virtualenv... " )
try :
run ( [ str ( python_path ) , " -m " , " ensurepip " , " --upgrade " ] )
except subprocess . CalledProcessError as exc :
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
# Ensure a local venv is present and use it for subsequent installs.
venv_python = _ensure_local_venv ( )
2025-12-31 16:10:35 -08:00
if not args . quiet :
print ( f " Using venv python: { venv_python } " )
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 :
if not playwright_package_installed ( ) :
2025-12-31 16:10:35 -08:00
if not args . quiet :
print ( " ' playwright ' package not found; installing it via pip... " )
2025-12-25 05:10:39 -08:00
run ( [ sys . executable , " -m " , " pip " , " install " , " playwright " ] )
2025-12-31 16:10:35 -08:00
if not args . quiet :
print (
" Installing Playwright browsers (this may download several hundred MB)... "
)
2025-12-25 05:10:39 -08:00
try :
cmd = _build_playwright_install_cmd ( args . browsers )
except ValueError as exc :
print ( f " Error: { exc } " , file = sys . stderr )
return 2
run ( cmd )
2025-12-31 16:10:35 -08:00
if not args . quiet :
print ( " Playwright browsers installed successfully. " )
2025-12-25 05:10:39 -08:00
return 0
if args . upgrade_pip :
2025-12-31 16:10:35 -08:00
if not args . quiet :
print ( " Upgrading pip, setuptools, and wheel in local venv... " )
2025-12-29 17:05:03 -08:00
run (
[
str ( venv_python ) ,
" -m " ,
" pip " ,
" install " ,
" --upgrade " ,
" pip " ,
" setuptools " ,
" wheel " ,
]
)
2025-12-25 05:10:39 -08:00
if not args . skip_deps :
2025-12-31 22:05:25 -08:00
req_file = repo_root / " scripts " / " requirements.txt "
2025-12-25 05:10:39 -08:00
if not req_file . exists ( ) :
2025-12-29 17:05:03 -08:00
print (
f " requirements.txt not found at { req_file } ; skipping dependency installation. " ,
file = sys . stderr ,
)
2025-12-25 05:10:39 -08:00
else :
2025-12-31 16:10:35 -08:00
if not args . quiet :
print (
f " Installing Python dependencies into local venv from { req_file } ... "
)
2025-12-25 05:10:39 -08:00
run ( [ str ( venv_python ) , " -m " , " pip " , " install " , " -r " , str ( req_file ) ] )
if not args . no_playwright :
if not playwright_package_installed ( ) :
2025-12-31 16:10:35 -08:00
if not args . quiet :
print ( " ' playwright ' package not installed in venv; installing it... " )
2025-12-25 05:10:39 -08:00
run ( [ str ( venv_python ) , " -m " , " pip " , " install " , " playwright " ] )
2025-12-31 16:10:35 -08:00
if not args . quiet :
print (
" Installing Playwright browsers (this may download several hundred MB)... "
)
2025-12-25 05:10:39 -08:00
try :
cmd = _build_playwright_install_cmd ( args . browsers )
except ValueError as exc :
print ( f " Error: { exc } " , file = sys . stderr )
return 2
# Run Playwright install using the venv's python so binaries are available in venv
cmd [ 0 ] = str ( venv_python )
run ( cmd )
# Install the project into the local venv (editable mode is the default, opinionated)
2025-12-31 16:10:35 -08:00
if not args . quiet :
print ( " Installing project into local venv (editable mode) " )
2025-12-31 22:58:54 -08:00
run ( [ str ( venv_python ) , " -m " , " pip " , " install " , " -e " , str ( repo_root / " scripts " ) ] )
2025-12-25 05:10:39 -08:00
# Verify top-level 'CLI' import and, if missing, attempt to make it available
2025-12-31 16:10:35 -08:00
if not args . quiet :
print ( " Verifying top-level ' CLI ' import in venv... " )
2025-12-25 05:10:39 -08:00
try :
rc = subprocess . run (
2025-12-29 18:42:02 -08:00
[
str ( venv_python ) ,
" -c " ,
" import importlib; importlib.import_module( ' CLI ' ) "
] ,
2025-12-25 05:10:39 -08:00
check = False ,
)
if rc . returncode == 0 :
print ( " OK: top-level ' CLI ' is importable in the venv. " )
else :
print (
" Top-level ' CLI ' not importable; attempting to add repo path to venv site-packages via a .pth file... "
)
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 "
) ,
]
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
if site_dir is None :
2025-12-29 17:05:03 -08:00
print (
" Could not determine venv site-packages directory; skipping .pth fallback "
)
2025-12-25 05:10:39 -08:00
else :
pth_file = site_dir / " medeia_repo.pth "
if pth_file . exists ( ) :
txt = pth_file . read_text ( encoding = " utf-8 " )
if str ( repo_root ) in txt :
print ( f " .pth already contains repo root: { pth_file } " )
else :
with pth_file . open ( " a " , encoding = " utf-8 " ) as fh :
fh . write ( str ( repo_root ) + " \n " )
print ( f " Appended repo root to existing .pth: { pth_file } " )
else :
with pth_file . open ( " w " , encoding = " utf-8 " ) as fh :
fh . write ( str ( repo_root ) + " \n " )
2025-12-29 18:42:02 -08:00
print (
f " Wrote .pth adding repo root to venv site-packages: { pth_file } "
)
2025-12-25 05:10:39 -08:00
# Re-check whether CLI can be imported now
rc2 = subprocess . run (
2025-12-29 17:05:03 -08:00
[
str ( venv_python ) ,
" -c " ,
" import importlib; importlib.import_module( ' CLI ' ) " ,
] ,
check = False ,
2025-12-25 05:10:39 -08:00
)
if rc2 . returncode == 0 :
print ( " Top-level ' CLI ' import works after adding .pth " )
else :
print (
" Adding .pth did not make top-level ' CLI ' importable; consider creating an egg-link or checking the venv. "
)
except Exception as exc :
2025-12-29 18:42:02 -08:00
print (
f " Warning: failed to verify or modify site-packages for top-level CLI: { exc } "
)
2025-12-25 05:10:39 -08:00
# Optional: install Deno runtime (default: install unless --no-deno is passed)
install_deno_requested = True
if getattr ( args , " no_deno " , False ) :
install_deno_requested = False
elif getattr ( args , " install_deno " , False ) :
install_deno_requested = True
if install_deno_requested :
2025-12-31 16:10:35 -08:00
if not args . quiet :
print ( " Installing Deno runtime (local/system)... " )
2025-12-25 05:10:39 -08:00
rc = _install_deno ( args . deno_version )
if rc != 0 :
print ( " Deno installation failed. " , file = sys . stderr )
return rc
2025-12-31 22:05:25 -08:00
# Write project-local launcher script under scripts/ to keep the repo root uncluttered.
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
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 ( )
# Install user-global shims so `mm` can be executed from any shell session.
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 15:41:38 -08:00
# Convert repo path to string with proper escaping
repo_str = str ( repo ) . replace ( " \\ " , " \\ \\ " )
2025-12-25 05:10:39 -08:00
# Write mm.ps1 (PowerShell shim)
mm_ps1 = user_bin / " mm.ps1 "
ps1_text = (
" Param([Parameter(ValueFromRemainingArguments=$true)] $args) \n "
2026-01-09 15:41:38 -08:00
f ' $repo = " { repo_str } " \n '
2025-12-25 05:10:39 -08:00
" $venv = Join-Path $repo ' .venv ' \n "
" $py = Join-Path $venv ' Scripts \\ python.exe ' \n "
" if (Test-Path $py) { \n "
" if ($env:MM_DEBUG) { \n "
2026-01-09 13:41:18 -08:00
' Write-Host " MM_DEBUG: using venv python at $py " -ForegroundColor Yellow \n '
2026-01-09 15:41:38 -08:00
" & $py -c \" import sys; print( ' sys.executable: ' , sys.executable); print( ' sys.path: ' , sys.path[:5]) \" \n "
2025-12-25 05:10:39 -08:00
" } \n "
2026-01-01 00:54:03 -08:00
" & $py -m scripts.cli_entry @args; exit $LASTEXITCODE \n "
2025-12-25 05:10:39 -08:00
" } \n "
2026-01-09 13:41:18 -08:00
" # Fallback to system python if venv doesn ' t exist \n "
2026-01-09 15:41:38 -08:00
" if ($env:MM_DEBUG) { Write-Host ' MM_DEBUG: venv python not found at ' $py ' , trying system python ' -ForegroundColor Yellow } \n "
2026-01-01 00:54:03 -08:00
" python -m scripts.cli_entry @args \n "
2025-12-25 05:10:39 -08:00
)
if mm_ps1 . exists ( ) :
bak = mm_ps1 . with_suffix ( f " .bak { int ( time . time ( ) ) } " )
mm_ps1 . replace ( bak )
mm_ps1 . write_text ( ps1_text , encoding = " utf-8 " )
2026-01-09 13:41:18 -08:00
# Write mm.bat (CMD shim for better compatibility)
mm_bat = user_bin / " mm.bat "
2026-01-09 15:41:38 -08:00
repo_bat_str = str ( repo )
2026-01-09 13:41:18 -08:00
bat_text = (
" @echo off \n "
" setlocal enabledelayedexpansion \n "
2026-01-09 15:41:38 -08:00
f ' set " REPO= { repo_bat_str } " \n '
2026-01-09 13:41:18 -08:00
" set \" VENV=!REPO! \\ .venv \" \n "
" set \" PY=!VENV! \\ Scripts \\ python.exe \" \n "
" if exist \" !PY! \" ( \n "
" if defined MM_DEBUG ( \n "
" echo MM_DEBUG: using venv python at !PY! \n "
2026-01-09 15:41:38 -08:00
" \" !PY! \" -c \" import sys; print( ' sys.executable: ' , sys.executable); print( ' sys.path: ' , sys.path[:5]) \" \n "
2026-01-09 13:41:18 -08:00
" ) \n "
" \" !PY! \" -m scripts.cli_entry % * \n "
" exit /b !ERRORLEVEL! \n "
" ) \n "
2026-01-09 15:41:38 -08:00
" echo MM: venv python not found at !PY! \n "
2026-01-09 13:41:18 -08:00
" if defined MM_DEBUG echo MM_DEBUG: venv python not found, trying system python \n "
" python -m scripts.cli_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 " )
# 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
if not args . quiet :
print ( f " Added { user_bin } to current session 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* \" ) {{ "
" [Environment]::SetEnvironmentVariable( ' PATH ' , ($bin + ' ; ' + ($cur -ne $null ? $cur : ' ' )), ' User ' ); "
" Write-Host ' Added {bin} to User PATH in registry ' -ForegroundColor Green "
" }} "
) . format ( bin = str_bin . replace ( " \\ " , " \\ \\ " ) )
subprocess . run (
[ " powershell " ,
" -NoProfile " ,
" -Command " ,
ps_cmd ] ,
check = False ,
stdout = subprocess . DEVNULL ,
stderr = subprocess . DEVNULL
)
except Exception as e :
if not args . quiet :
print ( f " Note: Could not persist PATH to registry (this is non-critical): { e } " , file = sys . stderr )
2025-12-25 05:10:39 -08:00
2025-12-31 16:10:35 -08:00
if not args . quiet :
2026-01-09 13:41:18 -08:00
print ( f " \n Installed global launchers to: { user_bin } " )
print ( f " ✓ mm.ps1 (PowerShell) " )
print ( f " ✓ mm.bat (Command Prompt) " )
print ( f " \n You can now run ' mm ' from any terminal window. " )
print ( f " To use in the current terminal, reload your profile or run: $env:PATH = ' { str_bin } ; ' + $env:PATH " )
2025-12-25 05:10:39 -08:00
else :
# POSIX
2025-12-29 18:42:02 -08:00
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 "
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 ' \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 "
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 "
" # Packaged console script in the venv if available \n "
2025-12-29 17:05:03 -08:00
' if [ -x " $VENV/bin/mm " ]; then \n '
' exec " $VENV/bin/mm " " $@ " \n '
2025-12-25 05:10:39 -08:00
" fi \n "
" # 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 )
print ( " Setup complete. " )
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 ( ) )