#!/usr/bin/env bash # # pi-imgen — installazione per macOS su Apple Silicon. # # Ri-eseguibile per costruzione: ogni passo controlla prima di agire, ripara invece di # duplicare, e NON sovrascrive mai i preset dell'utente in ~/.pi/agent/pi-imgen.json. # # NOTE FOR MAINTAINERS (English, per CODE STYLE; all user-visible strings are Italian): # - macOS ships bash 3.2, so: no associative arrays, no ${var,,}, no `read -i`, # and empty arrays must be guarded with ${#arr[@]} before expanding under `set -u`. # - The gRPCServerCLI install location and pinned version MUST stay in sync with # extensions/imgen/backends/server.ts (GRPC_SERVER_VERSION / serverPaths()), which # defaults the binary to /imgen/gRPCServerCLI-macOS. We deliberately do # NOT write into a Homebrew prefix we do not own, and we deliberately leave # `server.binary` unset in the config so the code default keeps the two in step. # - We never invent CLI flags. `draw-things-cli models ensure` is attempted ONLY if the # locally installed CLI's own --help confirms both the subcommand and the flags; see # prefetch_models(). Otherwise we print exact manual instructions and say so. # - No sudo is ever run by this script. The only privileged step is the official # Homebrew installer, which asks for its own password and only after we ask first. set -euo pipefail # --------------------------------------------------------------------------- # Costanti # --------------------------------------------------------------------------- REPO_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" # Sincronizzati con extensions/imgen/backends/server.ts GRPC_SERVER_VERSION="v1.20260716.0" GRPC_SERVER_BINARY="gRPCServerCLI-macOS" GRPC_SERVER_URL="https://github.com/drawthingsai/draw-things-community/releases/download/${GRPC_SERVER_VERSION}/${GRPC_SERVER_BINARY}" MODELS_CATALOGUE_URL="https://models.drawthings.ai/models.json" PI_CONFIG_DIR="${PI_CONFIG_DIR:-$HOME/.pi/agent}" STATE_DIR="$PI_CONFIG_DIR/imgen" CFG_PATH="$PI_CONFIG_DIR/pi-imgen.json" BREW_REQUIRED="draw-things-cli typst" BREW_OPTIONAL="ghostscript poppler qpdf" # Spazio consigliato per i modelli (~10 GB di pesi + margine di lavoro). MIN_MODELS_GB=15 DEFAULT_OUT_DIR="$HOME/Immagini/generated" # --------------------------------------------------------------------------- # Interfaccia # --------------------------------------------------------------------------- if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then C_RESET=$'\033[0m'; C_BOLD=$'\033[1m'; C_DIM=$'\033[2m' C_RED=$'\033[31m'; C_GREEN=$'\033[32m'; C_YELLOW=$'\033[33m'; C_BLUE=$'\033[34m' else C_RESET=""; C_BOLD=""; C_DIM=""; C_RED=""; C_GREEN=""; C_YELLOW=""; C_BLUE="" fi STEP_NO=0 step() { STEP_NO=$((STEP_NO + 1)); printf '\n%s[%d/8] %s%s\n' "$C_BOLD$C_BLUE" "$STEP_NO" "$1" "$C_RESET"; } ok() { printf ' %s✓%s %s\n' "$C_GREEN" "$C_RESET" "$1"; } info() { printf ' %s·%s %s\n' "$C_DIM" "$C_RESET" "$1"; } warn() { printf ' %s!%s %s\n' "$C_YELLOW" "$C_RESET" "$1"; } err() { printf ' %s✗%s %s\n' "$C_RED" "$C_RESET" "$1" >&2; } hint() { printf ' %s%s%s\n' "$C_DIM" "$1" "$C_RESET"; } die() { printf '\n%s✗ Installazione interrotta.%s %s\n\n' "$C_RED$C_BOLD" "$C_RESET" "$1" >&2 exit 1 } # Esiti raccolti per il riepilogo finale. SUMMARY_LINES=() ESSENTIAL_MISSING=0 record() { # record SUMMARY_LINES+=("$1"$'\t'"$2"$'\t'"$3") [ "$1" = "fail" ] && ESSENTIAL_MISSING=$((ESSENTIAL_MISSING + 1)) return 0 } # --------------------------------------------------------------------------- # Opzioni # --------------------------------------------------------------------------- ASSUME_YES=0 DOCTOR_ONLY=0 SKIP_MODELS=0 ARG_MODELS_DIR="" ARG_OUT_DIR="" usage() { cat <<'USAGE' pi-imgen — installazione (macOS, Apple Silicon) Uso: ./install.sh [opzioni] -y, --yes non fare domande: usa i valori predefiniti (o quelli già presenti nella configurazione). Utile per riesecuzioni. --models-dir DIR cartella dei modelli (deve stare su un disco secondario) --out-dir DIR cartella dei file generati --skip-models non prescaricare i modelli --doctor esegui solo il controllo finale, senza installare nulla -h, --help mostra questo messaggio Lo script è ri-eseguibile: ripara ciò che manca e non tocca i tuoi preset. USAGE } while [ $# -gt 0 ]; do case "$1" in -y|--yes) ASSUME_YES=1 ;; --doctor) DOCTOR_ONLY=1 ;; --skip-models) SKIP_MODELS=1 ;; --models-dir) [ $# -ge 2 ] || die "Manca il valore per --models-dir."; ARG_MODELS_DIR="$2"; shift ;; --models-dir=*) ARG_MODELS_DIR="${1#*=}" ;; --out-dir) [ $# -ge 2 ] || die "Manca il valore per --out-dir."; ARG_OUT_DIR="$2"; shift ;; --out-dir=*) ARG_OUT_DIR="${1#*=}" ;; -h|--help) usage; exit 0 ;; *) usage >&2; die "Opzione sconosciuta: $1" ;; esac shift done INTERACTIVE=1 if [ ! -t 0 ] || [ "$ASSUME_YES" = "1" ]; then INTERACTIVE=0; fi # --------------------------------------------------------------------------- # Utilità # --------------------------------------------------------------------------- have() { command -v "$1" >/dev/null 2>&1; } expand_tilde() { # stampa il percorso con ~ espansa case "$1" in "~") printf '%s' "$HOME" ;; "~/"*) printf '%s/%s' "$HOME" "${1#\~/}" ;; *) printf '%s' "$1" ;; esac } ask() { # ask -> stampa la risposta local question="$1" default="$2" reply="" if [ "$INTERACTIVE" = "0" ]; then printf '%s' "$default"; return 0; fi printf ' %s%s%s\n [%s]: ' "$C_BOLD" "$question" "$C_RESET" "$default" >&2 IFS= read -r reply || reply="" [ -n "$reply" ] || reply="$default" printf '%s' "$reply" } confirm() { # confirm -> 0 = sì local question="$1" default="$2" reply="" if [ "$INTERACTIVE" = "0" ]; then [ "$default" = "s" ]; return; fi local prompt="s/N"; [ "$default" = "s" ] && prompt="S/n" printf ' %s%s%s [%s]: ' "$C_BOLD" "$question" "$C_RESET" "$prompt" >&2 IFS= read -r reply || reply="" [ -n "$reply" ] || reply="$default" case "$reply" in [sSyY]*) return 0 ;; *) return 1 ;; esac } free_gb() { # spazio libero in GB; stampa sempre un numero, 0 se non si sa local gb gb="$(df -Pk "$1" 2>/dev/null | awk 'NR==2 { printf "%.0f", $4 / 1048576 }')" case "$gb" in ''|*[!0-9]*) printf '0' ;; *) printf '%s' "$gb" ;; esac } # Un file scaricato è davvero un eseguibile macOS? Stessi magic number di server.ts: # difende da una pagina di errore HTML o da un puntatore Git LFS serviti con 200 OK. is_macho() { local magic magic="$(head -c 4 "$1" 2>/dev/null | od -An -tx1 | tr -d ' \n')" case "$magic" in feedfacf|cffaedfe|feedface|cefaedfe|cafebabe|bebafeca) return 0 ;; *) return 1 ;; esac } # Percorso scrivibile? Prova davvero a scrivere: -w mente su volumi di rete e su ACL. dir_is_writable() { local probe="$1/.pi-imgen-write-test.$$" if ( umask 077; : > "$probe" ) 2>/dev/null; then rm -f "$probe"; return 0; fi return 1 } # --------------------------------------------------------------------------- # 0. Configurazione esistente (letta prima di tutto: guida i valori predefiniti) # --------------------------------------------------------------------------- EXIST_CFG_VALID=0 EXIST_MODELS_PATH="" EXIST_OUT_DIR="" EXIST_SERVER_BINARY="" read_existing_config() { [ -f "$CFG_PATH" ] || return 0 have python3 || return 0 local out="" out="$(CFG_PATH="$CFG_PATH" python3 <<'PY' 2>/dev/null || true import json, os, shlex def emit(name, value): print("%s=%s" % (name, shlex.quote(value if isinstance(value, str) else ""))) try: with open(os.environ["CFG_PATH"], encoding="utf-8") as fh: cfg = json.load(fh) if not isinstance(cfg, dict): raise ValueError("root is not an object") except Exception: raise SystemExit(0) print("EXIST_CFG_VALID=1") emit("EXIST_MODELS_PATH", cfg.get("modelsPath", "")) emit("EXIST_OUT_DIR", cfg.get("outputDir", "")) server = cfg.get("server") if isinstance(cfg.get("server"), dict) else {} emit("EXIST_SERVER_BINARY", server.get("binary", "")) PY )" # `out` contiene solo assegnazioni con valori già quotati da shlex.quote(). [ -n "$out" ] && eval "$out" return 0 } # --------------------------------------------------------------------------- # 1. Sistema # --------------------------------------------------------------------------- check_system() { step "Controllo del sistema" local os arch os="$(uname -s)" arch="$(uname -m)" if [ "$os" != "Darwin" ]; then die "pi-imgen funziona solo su macOS: qui il sistema è «$os». Il motore di generazione (Draw Things) esiste soltanto per macOS su Apple Silicon." fi # Sotto Rosetta `uname -m` risponde x86_64 anche su un Mac Apple Silicon: distinguere # i due casi evita di dire a un utente M4 che il suo Mac non è compatibile. local translated=0 translated="$(sysctl -n sysctl.proc_translated 2>/dev/null || printf '0')" if [ "$translated" = "1" ]; then die "Questo terminale sta girando sotto Rosetta (emulazione Intel). Draw Things richiede un processo nativo arm64. Apri un terminale nativo, oppure rilancia così: arch -arm64 /bin/bash \"$REPO_DIR/install.sh\"" fi if [ "$arch" != "arm64" ]; then die "pi-imgen richiede un Mac Apple Silicon (M1 o successivi): qui l'architettura è «$arch». Sui Mac Intel il motore di generazione non è disponibile e non esiste un ripiego." fi local osver; osver="$(sw_vers -productVersion 2>/dev/null || printf 'sconosciuta')" ok "macOS $osver su Apple Silicon ($arch)." # draw-things-cli richiede macOS 13+. case "$osver" in 1[0-2].*|[0-9].*) warn "draw-things-cli richiede macOS 13 o successivo: questa è la $osver." ;; esac local membytes memgb membytes="$(sysctl -n hw.memsize 2>/dev/null || printf '0')" case "$membytes" in ''|*[!0-9]*) membytes=0 ;; esac memgb="$(( membytes / 1073741824 ))" if [ "$memgb" -gt 0 ]; then info "Memoria unificata: ${memgb} GB." if [ "$memgb" -le 16 ]; then info "Con 16 GB il decoding a riquadri (tiled) resta attivo: dimezza la memoria di picco." fi fi have python3 || die "Serve python3 (incluso in macOS) per aggiornare la configurazione senza perdere i tuoi preset." have curl || die "Serve curl (incluso in macOS) per scaricare il server." record ok "Sistema" "macOS $osver, Apple Silicon" } # --------------------------------------------------------------------------- # 2. Homebrew e pacchetti # --------------------------------------------------------------------------- BREW="" find_brew() { if have brew; then BREW="$(command -v brew)"; return 0; fi if [ -x /opt/homebrew/bin/brew ]; then BREW=/opt/homebrew/bin/brew; return 0; fi if [ -x /usr/local/bin/brew ]; then BREW=/usr/local/bin/brew; return 0; fi return 1 } ensure_homebrew() { step "Homebrew" if find_brew; then ok "Homebrew trovato: $BREW" else warn "Homebrew non è installato: serve per draw-things-cli e typst." hint "L'installazione ufficiale chiederà LA TUA PASSWORD (usa sudo per conto suo)." if ! confirm "Installo Homebrew adesso?" "s"; then record fail "Homebrew" "assente — installalo da https://brew.sh e rilancia" err "Salto Homebrew. Senza, draw-things-cli e typst non si possono installare." return 0 fi /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" \ || die "Installazione di Homebrew fallita. Installalo a mano da https://brew.sh e rilancia questo script." find_brew || die "Homebrew risulta installato ma non lo trovo nel PATH. Apri un nuovo terminale e rilancia." ok "Homebrew installato: $BREW" fi # Su Apple Silicon il prefisso nativo è /opt/homebrew. Un brew in /usr/local è la # versione Intel: funziona sotto Rosetta ma non fornirà bottiglie arm64. case "$BREW" in /usr/local/*) warn "Questo Homebrew sta in /usr/local: è l'installazione Intel. Per pacchetti arm64 serve quella in /opt/homebrew." ;; esac eval "$("$BREW" shellenv)" 2>/dev/null || true local f for f in $BREW_REQUIRED; do if "$BREW" list --formula --versions "$f" >/dev/null 2>&1; then ok "$f già installato ($("$BREW" list --formula --versions "$f" | head -1))." record ok "$f" "già presente" else info "Installo $f…" # NB: formula di Homebrew CORE. Niente tap personalizzati, niente --HEAD. if "$BREW" install "$f"; then ok "$f installato." record ok "$f" "installato ora" else err "Installazione di $f fallita." record fail "$f" "installazione fallita — riprova con: brew install $f" fi fi done # Facoltativi: servono solo al controllo pre-stampa del PDF. local missing_opt="" for f in $BREW_OPTIONAL; do "$BREW" list --formula --versions "$f" >/dev/null 2>&1 || missing_opt="$missing_opt $f" done missing_opt="${missing_opt# }" if [ -n "$missing_opt" ]; then info "Facoltativi per il controllo pre-stampa (verifica di TrimBox, abbondanza e caratteri incorporati): $missing_opt" hint "Senza, la locandina si genera lo stesso: salta solo la verifica del PDF." if confirm "Li installo?" "n"; then # shellcheck disable=SC2086 if "$BREW" install $missing_opt; then ok "Strumenti di pre-stampa installati." record ok "Pre-stampa" "$missing_opt installati" else warn "Installazione dei facoltativi fallita: si prosegue senza." record warn "Pre-stampa" "installazione fallita (facoltativo)" fi else info "Salto i facoltativi. Più avanti: brew install $missing_opt" record warn "Pre-stampa" "assenti (facoltativo): $missing_opt" fi else ok "Strumenti di pre-stampa già presenti ($BREW_OPTIONAL)." record ok "Pre-stampa" "completi" fi } # --------------------------------------------------------------------------- # 3. gRPCServerCLI (asset di release GitHub, NON in Homebrew) # --------------------------------------------------------------------------- SERVER_TARGET="" ensure_grpc_server() { step "Server residente ($GRPC_SERVER_BINARY)" # Se la configurazione indica già un percorso per il binario, rispettiamolo: è quello # che il codice userà (config.server.binary vince su serverPaths()). if [ -n "$EXIST_SERVER_BINARY" ]; then SERVER_TARGET="$(expand_tilde "$EXIST_SERVER_BINARY")" info "Percorso preso dalla configurazione esistente: $SERVER_TARGET" else SERVER_TARGET="$STATE_DIR/$GRPC_SERVER_BINARY" fi mkdir -p "$(dirname -- "$SERVER_TARGET")" local stamp="$SERVER_TARGET.version" info "Non è distribuito da Homebrew: brew install draw-things-cli dà solo il client." if [ -f "$SERVER_TARGET" ]; then if [ ! -x "$SERVER_TARGET" ]; then chmod +x "$SERVER_TARGET" 2>/dev/null || true info "Permesso di esecuzione ripristinato." fi if [ -x "$SERVER_TARGET" ] && is_macho "$SERVER_TARGET"; then local installed="" [ -f "$stamp" ] && installed="$(cat "$stamp" 2>/dev/null || printf '')" if [ "$installed" = "$GRPC_SERVER_VERSION" ]; then ok "Già presente e aggiornato ($GRPC_SERVER_VERSION): $SERVER_TARGET" record ok "Server gRPC" "$GRPC_SERVER_VERSION" return 0 fi if [ -z "$installed" ]; then # Installato a mano: è valido, quindi lo teniamo. Riscaricarlo a ogni # esecuzione sarebbe uno spreco, e questo script deve essere ri-eseguibile. ok "Già presente (installato a mano): $SERVER_TARGET" info "Versione non determinabile; quella attesa è $GRPC_SERVER_VERSION." hint "Per allinearlo: rm \"$SERVER_TARGET\" e rilancia ./install.sh" record warn "Server gRPC" "presente, versione ignota (attesa $GRPC_SERVER_VERSION)" return 0 fi warn "Presente una versione diversa (installata: $installed, attesa: $GRPC_SERVER_VERSION)." if ! confirm "Lo aggiorno?" "s"; then record warn "Server gRPC" "versione $installed (attesa $GRPC_SERVER_VERSION)" return 0 fi else warn "Il file esistente non è un eseguibile macOS valido: lo riscarico." fi fi if ! confirm "Scarico $GRPC_SERVER_BINARY $GRPC_SERVER_VERSION (~decine di MB) da GitHub?" "s"; then record fail "Server gRPC" "assente — scaricalo da $GRPC_SERVER_URL in $SERVER_TARGET" warn "Salto lo scaricamento. Senza il server ogni immagine ricarica il modello da zero." return 0 fi local tmp="$SERVER_TARGET.part.$$" info "Scarico da $GRPC_SERVER_URL" if ! curl -fL --progress-bar -o "$tmp" "$GRPC_SERVER_URL"; then rm -f "$tmp" err "Scaricamento fallito (rete o URL non raggiungibile)." hint "A mano: curl -fL -o \"$SERVER_TARGET\" \"$GRPC_SERVER_URL\" && chmod +x \"$SERVER_TARGET\"" record fail "Server gRPC" "scaricamento fallito" return 0 fi # Un 200 OK non garantisce un binario: verifichiamo dimensione e magic number. local size; size="$(wc -c < "$tmp" | tr -d ' ')" if [ "$size" -lt 1048576 ] || ! is_macho "$tmp"; then rm -f "$tmp" err "Il file scaricato non è un eseguibile macOS valido ($size byte): probabilmente una pagina di errore." record fail "Server gRPC" "file scaricato non valido" return 0 fi chmod 755 "$tmp" # Gatekeeper mette in quarantena i file scaricati: senza questo il primo avvio viene bloccato. /usr/bin/xattr -d com.apple.quarantine "$tmp" 2>/dev/null || true mv -f "$tmp" "$SERVER_TARGET" printf '%s\n' "$GRPC_SERVER_VERSION" > "$stamp" ok "Installato in $SERVER_TARGET ($GRPC_SERVER_VERSION)." record ok "Server gRPC" "$GRPC_SERVER_VERSION" } # --------------------------------------------------------------------------- # 4. Percorsi: modelli (disco secondario) e destinazione # --------------------------------------------------------------------------- MODELS_PATH="" OUT_DIR="" suggest_models_dir() { # Candidati: volumi montati sotto /Volumes che NON sono il disco di avvio. local root_dev v dev gb best_gb=0 best="" root_dev="$(df -P / 2>/dev/null | awk 'NR==2 { print $1 }')" for v in /Volumes/*; do [ -d "$v" ] || continue case "$v" in *com.apple.TimeMachine*|*.timemachine*) continue ;; esac dev="$(df -P "$v" 2>/dev/null | awk 'NR==2 { print $1 }')" if [ -z "$dev" ] || [ "$dev" = "$root_dev" ]; then continue; fi dir_is_writable "$v" || continue gb="$(free_gb "$v")" [ -n "$gb" ] || gb=0 if [ "$gb" -gt "$best_gb" ]; then best_gb="$gb"; best="$v"; fi done if [ -n "$best" ]; then printf '%s/pi-imgen/models' "$best"; return 0; fi # Nessun disco secondario montato: proponiamo comunque un percorso fuori dalla home # se esiste, altrimenti la home (chiedendo conferma esplicita più avanti). printf '%s' "$HOME/Documents/Models" } prompt_paths() { step "Dove tenere i modelli e i file generati" # -- modelli --------------------------------------------------------------- local default_models="" if [ -n "$ARG_MODELS_DIR" ]; then default_models="$ARG_MODELS_DIR" elif [ -n "$EXIST_MODELS_PATH" ]; then default_models="$EXIST_MODELS_PATH" info "Configurazione attuale: $EXIST_MODELS_PATH" else default_models="$(suggest_models_dir)" fi printf ' %sI modelli occupano circa 10 GB. Tienili su un disco secondario o esterno,%s\n' "$C_DIM" "$C_RESET" printf ' %snon nella cartella utente: così i backup di casa restano leggeri.%s\n' "$C_DIM" "$C_RESET" local candidates="" v for v in /Volumes/*; do [ -d "$v" ] || continue case "$v" in *com.apple.TimeMachine*) continue ;; esac candidates="$candidates $v ($(free_gb "$v") GB liberi)" done [ -n "$candidates" ] && info "Volumi montati:$candidates" while :; do local answer answer="$(ask "Cartella dei modelli" "$default_models")" answer="$(expand_tilde "$answer")" case "$answer" in /*) ;; *) err "Il percorso deve essere assoluto (deve iniziare con «/»). Ricevuto: «$answer»" if [ "$INTERACTIVE" = "0" ]; then die "Percorso dei modelli non assoluto: $answer"; fi continue ;; esac case "$answer" in "$HOME"|"$HOME"/*) warn "«$answer» sta nella tua cartella utente: finirà nei backup e in iCloud Drive." if [ "$INTERACTIVE" = "1" ] && ! confirm "Va bene lo stesso?" "n"; then default_models="$(suggest_models_dir)" continue fi ;; esac if ! mkdir -p "$answer" 2>/dev/null; then err "Non riesco a creare «$answer» (permessi, o disco non montato)." if [ "$INTERACTIVE" = "0" ]; then die "Cartella dei modelli non creabile: $answer"; fi continue fi if ! dir_is_writable "$answer"; then err "«$answer» non è scrivibile (volume in sola lettura?)." if [ "$INTERACTIVE" = "0" ]; then die "Cartella dei modelli non scrivibile: $answer"; fi continue fi local gb; gb="$(free_gb "$answer")"; [ -n "$gb" ] || gb=0 if [ "$gb" -lt "$MIN_MODELS_GB" ]; then warn "Spazio libero: ${gb} GB. Ne servono almeno ${MIN_MODELS_GB} per i modelli." if [ "$INTERACTIVE" = "1" ] && ! confirm "Proseguo comunque?" "n"; then continue; fi else info "Spazio libero: ${gb} GB." fi MODELS_PATH="$answer" ok "Modelli: $MODELS_PATH" break done # -- destinazione ---------------------------------------------------------- local default_out="" if [ -n "$ARG_OUT_DIR" ]; then default_out="$ARG_OUT_DIR" elif [ -n "$EXIST_OUT_DIR" ]; then default_out="$EXIST_OUT_DIR" else default_out="$DEFAULT_OUT_DIR"; fi while :; do local answer answer="$(ask "Cartella dei file generati" "$default_out")" answer="$(expand_tilde "$answer")" case "$answer" in /*) ;; *) err "Il percorso deve essere assoluto. Ricevuto: «$answer»" if [ "$INTERACTIVE" = "0" ]; then die "Cartella di destinazione non assoluta: $answer"; fi continue ;; esac if ! mkdir -p "$answer" 2>/dev/null || ! dir_is_writable "$answer"; then err "«$answer» non è utilizzabile: non riesco a scriverci." if [ "$INTERACTIVE" = "0" ]; then die "Cartella di destinazione non scrivibile: $answer"; fi continue fi OUT_DIR="$answer" ok "Destinazione: $OUT_DIR" break done record ok "Cartelle" "modelli in $MODELS_PATH, uscite in $OUT_DIR" } # --------------------------------------------------------------------------- # 5. Caratteri # --------------------------------------------------------------------------- FONT_SCRIPT="$REPO_DIR/scripts/fetch-fonts.sh" fetch_fonts() { step "Caratteri tipografici (22 famiglie OFL/Apache)" if [ ! -f "$FONT_SCRIPT" ]; then warn "Script assente: $FONT_SCRIPT" hint "Senza i caratteri, l'impaginazione con Typst non parte." record fail "Caratteri" "scripts/fetch-fonts.sh non trovato" return 0 fi [ -x "$FONT_SCRIPT" ] || chmod +x "$FONT_SCRIPT" 2>/dev/null || true info "Eseguo scripts/fetch-fonts.sh…" # Fallire qui non deve interrompere l'installazione: i caratteri si riscaricano dopo. if ( cd "$REPO_DIR" && /usr/bin/env bash "$FONT_SCRIPT" ); then local n; n="$(find "$REPO_DIR/vendor/fonts" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l | tr -d ' ')" ok "Caratteri pronti ($n famiglie in vendor/fonts)." record ok "Caratteri" "$n famiglie" else err "Scaricamento dei caratteri fallito." hint "Riprova più tardi con: \"$FONT_SCRIPT\"" record fail "Caratteri" "scaricamento fallito" fi } # --------------------------------------------------------------------------- # 6. Modelli # --------------------------------------------------------------------------- # ModelsConfig (extensions/imgen/config.ts) dichiara obbligatori SOLO draft e final: # alt, edit e upscale sono `?` e la loro assenza non deve far fallire nulla. MODEL_KEYS_REQUIRED="draft final" MODEL_KEYS_OPTIONAL="alt edit upscale" # Legge dalla configurazione scritta i modelli per le chiavi indicate, una riga # "chiavefile" per ciascuna chiave davvero presente e valorizzata. config_model_files() { # config_model_files ... if [ ! -f "$CFG_PATH" ] || ! have python3; then return 0 fi CFG_PATH="$CFG_PATH" MODEL_KEYS="$*" python3 <<'PY' 2>/dev/null || true import json, os try: with open(os.environ["CFG_PATH"], encoding="utf-8") as fh: models = json.load(fh).get("models", {}) if isinstance(models, dict): for key in os.environ["MODEL_KEYS"].split(): value = models.get(key) if isinstance(value, str) and value: print(f"{key}\t{value}") except Exception: pass PY } # Ripiego per una singola chiave: il valore in DEFAULTS.models dentro config.ts, # così l'elenco non viene duplicato qui dentro. default_model_file() { # default_model_file sed -n "s/^[[:space:]]*$1:[[:space:]]*\"\([A-Za-z0-9_.-]*\.ckpt\)\".*/\1/p" \ "$REPO_DIR/extensions/imgen/config.ts" 2>/dev/null | head -n 1 } # I file attesi per un gruppo di chiavi. L'unione è la stessa che fa loadConfig: # il file dell'utente vince chiave per chiave, il resto arriva da DEFAULTS. model_files() { # model_files ... local pairs key value pairs="$(config_model_files "$@")" for key in "$@"; do value="$(printf '%s\n' "$pairs" | awk -F'\t' -v k="$key" '$1 == k { print $2; exit }')" if [ -z "$value" ]; then value="$(default_model_file "$key")"; fi if [ -n "$value" ]; then printf '%s\n' "$value"; fi done | sort -u } required_model_files() { model_files $MODEL_KEYS_REQUIRED; } # I facoltativi già coperti dai necessari (per impostazione predefinita draft e # final sono lo stesso file) non vanno riproposti come «mancanti». optional_model_files() { local required required="$(required_model_files)" # grep -F con più righe = un motivo per riga: scarta i doppioni dei necessari. model_files $MODEL_KEYS_OPTIONAL | grep -vxF "$required" || true } # Avviso, mai errore: senza questi si genera lo stesso, si perdono solo lo stile # alternativo, le modifiche (img2img) e l'ingrandimento. report_optional_models() { local m missing="" have_any=0 for m in $(optional_model_files); do if [ -f "$MODELS_PATH/$m" ]; then have_any=1 else missing="$missing $m" fi done missing="${missing# }" if [ -z "$missing" ]; then if [ "$have_any" = "1" ]; then ok "Anche i modelli facoltativi sono presenti." record ok "Modelli extra" "presenti" fi return 0 fi info "Facoltativi non presenti (nessun problema): $missing" hint "Servono solo per lo stile alternativo, le modifiche e l'ingrandimento." hint "Puoi aggiungerli quando ti servono, dall'app Draw Things o dal catalogo." record warn "Modelli extra" "facoltativi assenti: $missing" } prefetch_models() { step "Modelli di generazione" if [ "$SKIP_MODELS" = "1" ]; then info "Salto lo scaricamento dei modelli (--skip-models)." record warn "Modelli" "scaricamento saltato" return 0 fi local wanted present=0 wanted="$(required_model_files)" if [ -z "$wanted" ]; then warn "Non riesco a determinare quali modelli servono." record warn "Modelli" "elenco non determinato" return 0 fi local m missing="" for m in $wanted; do if [ -f "$MODELS_PATH/$m" ]; then ok "Già presente: $m" present=$((present + 1)) else missing="$missing $m" fi done missing="${missing# }" if [ -z "$missing" ]; then ok "I modelli necessari sono già in $MODELS_PATH." record ok "Modelli" "$present presenti" report_optional_models return 0 fi info "Mancano: $missing" if ! have draw-things-cli; then warn "draw-things-cli non è installato: non posso prescaricare nulla." document_model_download record fail "Modelli" "mancanti: $missing" report_optional_models return 0 fi # NON inventiamo flag. Interroghiamo l'aiuto del CLI installato e usiamo il # sottocomando SOLO se l'aiuto stesso conferma sia «ensure» sia «--models-dir». local root_help models_help="" root_help="$(draw-things-cli --help 2>&1 || true)" if printf '%s' "$root_help" | grep -qE '(^|[[:space:]])models([[:space:]]|$)'; then models_help="$(draw-things-cli models --help 2>&1 || true)" fi if [ -n "$models_help" ] \ && printf '%s' "$models_help" | grep -qE '(^|[[:space:]])ensure([[:space:]]|$)' \ && printf '%s' "$models_help" | grep -q -- '--models-dir'; then info "Il CLI dichiara «models ensure»: uso la forma verificata dal suo --help." local still="" for m in $missing; do info "Scarico $m…" # Un'uscita a 0 non basta: il file su disco è l'unica prova che serve. draw-things-cli models ensure --models-dir "$MODELS_PATH" "$m" || true if [ -s "$MODELS_PATH/$m" ]; then ok "$m pronto." else warn "$m non è arrivato in $MODELS_PATH." still="$still $m" fi done still="${still# }" if [ -z "$still" ]; then record ok "Modelli" "tutti presenti" else document_model_download record fail "Modelli" "mancanti: $still" fi report_optional_models return 0 fi # Il sottocomando non esiste (o ha un'altra forma) in questa versione del CLI. warn "Questa versione di draw-things-cli non espone «models ensure» nel suo aiuto." info "Non tiro a indovinare i parametri: qui sotto trovi le vie sicure." document_model_download record fail "Modelli" "mancanti: $missing" report_optional_models } document_model_download() { cat </dev/null || printf '0')" if [ "$kept" != "0" ]; then ok "Preset conservati: $kept."; else info "Nessun preset salvato (li creerai con /presets)."; fi record ok "Configurazione" "$CFG_PATH" else err "Scrittura della configurazione fallita." record fail "Configurazione" "scrittura fallita in $CFG_PATH" fi } # --------------------------------------------------------------------------- # 8. Controllo finale # --------------------------------------------------------------------------- doctor() { step "Controllo finale" local v # -- strumenti ------------------------------------------------------------- if have draw-things-cli; then v="$(draw-things-cli --version 2>&1 | head -1 || true)" [ -n "$v" ] || v="presente" ok "draw-things-cli: $v" else err "draw-things-cli non trovato nel PATH." hint "brew install draw-things-cli" fi if have typst; then ok "typst: $(typst --version 2>&1 | head -1)" else err "typst non trovato nel PATH." hint "brew install typst" fi if have node; then local nodemajor; nodemajor="$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0)" if [ "$nodemajor" -ge 20 ] 2>/dev/null; then ok "Node $(node -v)." else warn "Node $(node -v): pi-imgen richiede Node 20 o successivo." fi else warn "Node non trovato: lo fornisce pi, di solito non è un problema." fi # -- server ---------------------------------------------------------------- if [ -n "$SERVER_TARGET" ] && [ -x "$SERVER_TARGET" ] && is_macho "$SERVER_TARGET"; then ok "Server residente pronto: $SERVER_TARGET" else err "Server residente assente o non eseguibile." hint "curl -fL -o \"${SERVER_TARGET:-$STATE_DIR/$GRPC_SERVER_BINARY}\" \"$GRPC_SERVER_URL\" && chmod +x \"${SERVER_TARGET:-$STATE_DIR/$GRPC_SERVER_BINARY}\"" fi # Porta 7859: già occupata significa quasi sempre un server già avviato — bene così, # pi-imgen adotta quello esistente invece di avviarne un secondo (due processi di # diffusione saturano 16 GB prima di finire una decodifica). local port=7859 if have python3; then port="$(CFG_PATH="$CFG_PATH" python3 -c 'import json,os;print((json.load(open(os.environ["CFG_PATH"])).get("server") or {}).get("port",7859))' 2>/dev/null || printf '7859')" fi if have nc && nc -z 127.0.0.1 "$port" >/dev/null 2>&1; then info "Porta $port: qualcosa è già in ascolto (server già avviato: verrà adottato)." else info "Porta $port libera: il server partirà all'avvio della sessione pi." fi # -- cartelle e modelli ---------------------------------------------------- if [ -d "$MODELS_PATH" ] && dir_is_writable "$MODELS_PATH"; then ok "Cartella modelli: $MODELS_PATH ($(free_gb "$MODELS_PATH") GB liberi)." else err "Cartella modelli non utilizzabile: $MODELS_PATH" fi if [ -d "$OUT_DIR" ] && dir_is_writable "$OUT_DIR"; then ok "Cartella di destinazione: $OUT_DIR" else err "Cartella di destinazione non utilizzabile: $OUT_DIR" fi # Necessari (draft, final) e facoltativi (alt, edit, upscale) contati a parte: # i secondi non rendono l'installazione incompleta. local m found=0 lost=0 for m in $(required_model_files); do if [ -f "$MODELS_PATH/$m" ]; then found=$((found + 1)); else lost=$((lost + 1)); fi done if [ "$lost" = "0" ] && [ "$found" -gt 0 ]; then ok "Modelli necessari: $found presenti." else warn "Modelli necessari: $found presenti, $lost mancanti." fi local extra_found=0 extra_lost=0 for m in $(optional_model_files); do if [ -f "$MODELS_PATH/$m" ]; then extra_found=$((extra_found + 1)) else extra_lost=$((extra_lost + 1)) fi done if [ "$extra_lost" = "0" ]; then if [ "$extra_found" -gt 0 ]; then ok "Modelli facoltativi: $extra_found presenti."; fi else info "Modelli facoltativi: $extra_found presenti, $extra_lost non scaricati (va bene così)." fi # -- caratteri ------------------------------------------------------------- local nf; nf="$(find "$REPO_DIR/vendor/fonts" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l | tr -d ' ')" if [ "$nf" -ge 22 ]; then ok "Caratteri: $nf famiglie." elif [ "$nf" -gt 0 ]; then warn "Caratteri: solo $nf famiglie su 22." else err "Caratteri assenti: esegui \"$FONT_SCRIPT\"." fi # -- configurazione -------------------------------------------------------- if [ -f "$CFG_PATH" ] && C="$CFG_PATH" python3 -c 'import json,os;json.load(open(os.environ["C"]))' >/dev/null 2>&1; then ok "Configurazione leggibile: $CFG_PATH" else err "Configurazione assente o illeggibile: $CFG_PATH" fi } print_summary() { printf '\n%s────────────────────────────────────────────────────────────%s\n' "$C_DIM" "$C_RESET" printf '%sRiepilogo%s\n\n' "$C_BOLD" "$C_RESET" local line status label detail mark color if [ "${#SUMMARY_LINES[@]}" -gt 0 ]; then for line in "${SUMMARY_LINES[@]}"; do status="${line%%$'\t'*}" label="$(printf '%s' "$line" | cut -f2)" detail="$(printf '%s' "$line" | cut -f3)" case "$status" in ok) mark="pronto" ; color="$C_GREEN" ;; warn) mark="parziale" ; color="$C_YELLOW" ;; *) mark="da sistemare" ; color="$C_RED" ;; esac printf ' %s%-12s%s %-16s %s\n' "$color" "$mark" "$C_RESET" "$label" "$detail" done fi printf '\n' if [ "$ESSENTIAL_MISSING" = "0" ]; then printf '%s✓ Tutto pronto.%s\n\n' "$C_GREEN$C_BOLD" "$C_RESET" printf ' Apri pi e prova:\n' printf ' %s/poster%s una locandina completa, dal titolo al PDF per la stampa\n' "$C_BOLD" "$C_RESET" printf ' %s/presets%s salva i tuoi colori, caratteri e logo\n\n' "$C_BOLD" "$C_RESET" else if [ "$ESSENTIAL_MISSING" = "1" ]; then printf '%s! Manca ancora una cosa.%s\n\n' "$C_YELLOW$C_BOLD" "$C_RESET" else printf '%s! Mancano ancora %d cose.%s\n\n' "$C_YELLOW$C_BOLD" "$ESSENTIAL_MISSING" "$C_RESET" fi printf ' Sistema le voci «da sistemare» qui sopra e rilancia:\n' printf ' %s./install.sh%s (ripara ciò che manca, non tocca i tuoi preset)\n\n' "$C_BOLD" "$C_RESET" fi printf ' %sConfigurazione:%s %s\n' "$C_DIM" "$C_RESET" "$CFG_PATH" printf ' %sModelli:%s %s\n' "$C_DIM" "$C_RESET" "${MODELS_PATH:-non impostata}" printf ' %sDestinazione:%s %s\n\n' "$C_DIM" "$C_RESET" "${OUT_DIR:-non impostata}" } # --------------------------------------------------------------------------- # main # --------------------------------------------------------------------------- main() { printf '\n%spi-imgen — installazione%s\n' "$C_BOLD" "$C_RESET" printf '%sLocandine, loghi e immagini per eventi. Tutto in locale.%s\n' "$C_DIM" "$C_RESET" read_existing_config if [ "$DOCTOR_ONLY" = "1" ]; then MODELS_PATH="$(expand_tilde "${EXIST_MODELS_PATH:-$HOME/Documents/Models}")" OUT_DIR="$(expand_tilde "${EXIST_OUT_DIR:-$DEFAULT_OUT_DIR}")" SERVER_TARGET="${EXIST_SERVER_BINARY:-$STATE_DIR/$GRPC_SERVER_BINARY}" SERVER_TARGET="$(expand_tilde "$SERVER_TARGET")" STEP_NO=7 doctor printf '\n' exit 0 fi check_system ensure_homebrew ensure_grpc_server prompt_paths fetch_fonts prefetch_models write_config doctor print_summary [ "$ESSENTIAL_MISSING" = "0" ] || exit 1 } main "$@"