#!/usr/bin/env bash # Installa NomaDev su macOS, Linux o un VPS. # # curl -fsSL https://nomadev.app/install.sh | bash # curl -fsSL https://nomadev.app/install.sh | bash -s -- --vps # # Per ogni programma che manca chiede prima di installarlo (--si per non # chiedere). --prova stampa cosa farebbe senza fare niente. Rilanciarlo è # sicuro: salta quello che c'è già e non tocca la configurazione. # # Tutto sta dentro `principale`, chiamata solo all'ultima riga: con # `curl | bash` bash legge lo script dallo stesso stdin dei comandi, e un # programma che leggesse stdin si mangerebbe il resto dello script. Così bash # ha già letto tutto prima di eseguire qualcosa, e uno script scaricato a metà # non parte. set -euo pipefail SI=${NOMADEV_SI:-0}; PROVA=${NOMADEV_PROVA:-0}; VPS=0 SISTEMA=$(uname) passo() { printf '\n== %s\n' "$1"; } ok() { printf ' %s\n' "$1"; } attenzione() { printf ' ! %s\n' "$1"; } fermati() { printf '\n X %s\n\n' "$1" >&2; exit 1; } presente() { command -v "$1" >/dev/null 2>&1; } # Esegue un comando, o lo stampa in modalità prova. stdin da /dev/null: niente # di quello che gira qui deve leggere lo script (vedi sopra) o restare appeso # ad aspettare una risposta che con -y non serve. fai() { if [[ $PROVA == 1 ]]; then printf ' [prova] %s\n' "$*"; return 0; fi bash -c "$*" "$percorso" } # Chiede conferma; in prova o con --si risponde sì. Legge dal terminale anche # quando lo script arriva da `curl | bash`, dove stdin è lo script stesso. # Senza terminale non si installa niente a scatola chiusa: serve --si. chiedi() { [[ $SI == 1 || $PROVA == 1 ]] && return 0 [[ $TERMINALE == 1 ]] || fermati "Nessun terminale per chiedere conferma. Rilancia con --si per installare senza domande." local r read -r -p " $1 [S/n] " r /dev/null) || return 1 (( maggiore >= 20 )) } # Su macOS /usr/bin/git esiste sempre, ma senza gli strumenti da riga di # comando di Xcode apre solo la finestra che propone di installarli. git_ok() { presente git || return 1 [[ $SISTEMA == Darwin && $(command -v git) == /usr/bin/git ]] || return 0 xcode-select -p >/dev/null 2>&1 } claude_ok() { presente claude; } # Su macOS Tailscale è un'app: il comando `tailscale` può non essere nel PATH. APP_TAILSCALE=/Applications/Tailscale.app tailscale_bin() { if presente tailscale; then command -v tailscale; return; fi [[ -x $APP_TAILSCALE/Contents/MacOS/Tailscale ]] && echo "$APP_TAILSCALE/Contents/MacOS/Tailscale" } tailscale_ok() { [[ -n $(tailscale_bin) ]]; } # Claude Code tiene le credenziali in ~/.claude/.credentials.json su Linux e # nel Portachiavi su macOS; in entrambi i casi ~/.claude.json ricorda l'account. claude_collegato() { [[ -e "$HOME/.claude/.credentials.json" ]] || grep -q '"oauthAccount"' "$HOME/.claude.json" 2>/dev/null } # installa "Nome" funzione-che-controlla "comando di installazione" "link" installa() { local nome=$1 verifica=$2 comando=$3 link=$4 if [[ $PROVA == 0 ]] && $verifica; then ok "$nome: presente"; return; fi if [[ -z $comando ]]; then # In prova si va avanti comunque: deve mostrare tutti i passi, qualunque # cosa ci sia su questa macchina. if [[ $PROVA == 1 ]]; then printf ' [prova] Installo %s: qui non so come, mi fermerei col link %s\n' "$nome" "$link"; return; fi fermati "$nome manca e qui non so installarlo. Installalo da $link e rilancia questo comando." fi chiedi "Manca $nome. Lo installo?" || fermati "$nome serve. Installalo da $link e rilancia questo comando." printf ' Installo %s\n' "$nome" fai "$comando" || fermati "Installazione di $nome non riuscita. Installalo da $link e rilancia." [[ $PROVA == 1 ]] && return hash -r $verifica || fermati "$nome non risulta ancora pronto. Se si è aperta una finestra di installazione completala; poi apri un nuovo terminale e rilancia questo comando." } # Un valore dentro un file unit di systemd, tra virgolette: systemd vi legge # gli escape alla C e gli specificatori %, e in ExecStart anche le $. Con sed # e non con ${v//…}: le sostituzioni di bash cambiano tra la 3.2 di macOS e # la 5.2 (dove una & nella sostituzione vuol dire "il testo trovato"). systemd_val() { local dollari='s/x/x/' if [[ ${2:-} == exec ]]; then dollari='s/\$/$$/g'; fi printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/%/%%/g' -e "$dollari" } # Un valore dentro il plist di launchd, che è XML. xml_val() { printf '%s' "$1" | sed -e 's/&/\&/g' -e 's//\>/g' -e 's/"/\"/g' -e "s/'/\'/g" } # Un valore per uno script sh, tra apici singoli (un apice dentro diventa '\''). sh_val() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")" } # Un argomento tra virgolette dentro Exec= di un file .desktop: la specifica # vuole l'escape di \ " ` $ e % raddoppiato. desktop_val() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/`/\\`/g' -e 's/\$/\\$/g' -e 's/%/%%/g' } # Aggiunge ~/.local/bin al PATH dei prossimi terminali, se il file non lo # nomina già. RIGA_PATH='export PATH="$HOME/.local/bin:$PATH"' aggiungi_al_profilo() { local file=$1 if [[ $PROVA == 1 ]]; then printf ' [prova] (se npm passa a ~/.local) aggiungo a %s: %s\n' "$file" "$RIGA_PATH"; return; fi grep -qs '\.local/bin' "$file" && return printf '\n# NomaDev: comandi installati in ~/.local/bin\n%s\n' "$RIGA_PATH" >> "$file" ok "Aggiunto ~/.local/bin al PATH in $file" } # Le installazioni fatte con i vecchi script del repo (install/unix.sh) # registravano un servizio che lancia `npm start` dalla cartella server del # repo: freedev.service (systemd utente) o com.freedev.server (launchd). # Acceso insieme a quello nuovo si contenderebbe la porta 4000, e il nuovo non # partirebbe. Prima di registrare il nuovo si propone di toglierlo; in prova # si mostra sempre, perché si veda cosa farebbe. togli_vecchio_servizio() { local nome file togli if [[ $SISTEMA == Darwin ]]; then nome='com.freedev.server'; file="$HOME/Library/LaunchAgents/com.freedev.server.plist" togli="launchctl bootout gui/$(id -u)/com.freedev.server 2>/dev/null; rm -f '$file'" else nome='freedev.service'; file="$HOME/.config/systemd/user/freedev.service" togli="systemctl --user disable --now freedev.service 2>/dev/null; rm -f '$file'; systemctl --user daemon-reload" fi if [[ $PROVA == 1 ]]; then ok "(se c'è il servizio $nome di una vecchia installazione dal repo)" elif [[ ! -e $file ]]; then return 0 fi attenzione "C'è $nome, il servizio di una vecchia installazione dal repo: avvia il server con npm start e terrebbe occupata la porta 4000, così il nuovo non partirebbe." if ! chiedi "Lo fermo e lo tolgo?"; then attenzione "Lasciato. Finché è acceso il nuovo server non parte: toglilo con $togli" return 0 fi fai "$togli" || attenzione "Non sono riuscito a toglierlo del tutto. A mano: $togli" ok "Per tenere i telefoni già collegati: copia config.json e la cartella data/ dalla cartella server del vecchio repo in $NOMADEV_HOME, poi rilancia questo comando." } gestore() { if [[ $SISTEMA == Darwin ]]; then presente brew && echo brew || echo nessuno return fi if presente apt-get; then echo apt; elif presente dnf; then echo dnf; else echo nessuno; fi } principale() { local a for a in "$@"; do case "$a" in --si) SI=1 ;; --prova) PROVA=1 ;; --vps) VPS=1 ;; *) attenzione "Opzione sconosciuta, la ignoro: $a" ;; esac done case "$SISTEMA" in Darwin|Linux) ;; *) [[ $PROVA == 1 ]] || fermati "Questo script è per macOS e Linux. Su Windows: irm https://nomadev.app/install.ps1 | iex" ;; esac if (exec /dev/null; then TERMINALE=1; else TERMINALE=0; fi # `curl | sudo bash`: tutto finirebbe a root (servizio, configurazione, # accesso a Claude) invece che all'utente. Lo script il sudo lo chiede da # sé, solo dove serve. if [[ $(id -u) == 0 && -n ${SUDO_USER:-} ]]; then fermati "Non lanciarlo con sudo: rilancialo come $SUDO_USER, senza sudo. La password la chiede lo script quando serve." fi # Root vero (es. un VPS dove si entra come root): niente sudo davanti ai comandi. Il servizio però è dell'utente che # lancia lo script, quindi girerebbe come root: si avvisa, senza bloccare. local S='' SE='' if [[ $(id -u) == 0 ]]; then attenzione 'Stai installando come root: il server girerà come root, con i permessi di root sui tuoi progetti.' attenzione 'Meglio un utente normale: crealo (adduser nome), entra con quello e rilancia questo comando.' else S='sudo '; SE='sudo -E ' fi # Claude Code e, se serve, npm (vedi sotto) installano in ~/.local/bin: deve # essere nel PATH di questa sessione anche se non lo è ancora in quello # dell'utente. local LOCALE_NEL_PATH=1 case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) LOCALE_NEL_PATH=0; export PATH="$HOME/.local/bin:$PATH" ;; esac # La configurazione sta sempre lì, qualunque sia la cartella da cui si # lancia lo script o da cui parte il servizio: con una config.json nella # cartella corrente `nomadev` userebbe quella (vedi percorsi.ts). Chi ha # già scelto NOMADEV_HOME tiene la sua. export NOMADEV_HOME=${NOMADEV_HOME:-$HOME/.nomadev} passo 'Programmi necessari' local G NODE='' GIT='' TS='' G=$(gestore) case "$G" in brew) NODE="brew install node@22 && brew link --overwrite --force node@22"; GIT="brew install git" TS="brew install --cask tailscale-app || brew install --cask tailscale" ;; apt) NODE="curl -fsSL https://deb.nodesource.com/setup_22.x | ${SE}bash - && ${S}apt-get install -y nodejs"; GIT="${S}apt-get install -y git" ;; dnf) NODE="curl -fsSL https://rpm.nodesource.com/setup_22.x | ${SE}bash - && ${S}dnf install -y nodejs"; GIT="${S}dnf install -y git" ;; *) NODE=""; GIT="" ;; esac # Senza Homebrew, su macOS Node non si installa da qui (installare Homebrew # chiederebbe molto di più): installa() dà il link e si ferma. git sì: sono # gli strumenti di Xcode, che macOS installa da sé con una finestra. [[ $SISTEMA == Darwin && $G != brew ]] && GIT="xcode-select --install" # Lo script ufficiale di Tailscale conosce quasi tutte le distribuzioni. [[ $SISTEMA != Darwin ]] && TS="curl -fsSL https://tailscale.com/install.sh | sh" # node_ok controlla anche la versione: una Node vecchia si aggiorna con lo # stesso comando che la installa. installa 'Node.js 20 o più recente' node_ok "$NODE" 'https://nodejs.org' installa 'git' git_ok "$GIT" 'https://git-scm.com/downloads' installa 'Claude Code' claude_ok 'curl -fsSL https://claude.ai/install.sh | bash' 'https://claude.ai/download' if [[ $VPS == 0 ]]; then installa 'Tailscale' tailscale_ok "$TS" 'https://tailscale.com/download' fi passo 'Accessi' if [[ $PROVA == 1 ]] || ! claude_collegato; then ok "Fai l'accesso a Claude con il tuo abbonamento (si apre il browser), poi esci con /exit." fai_al_terminale 'claude' || attenzione "Accesso a Claude non completato: lancia \`claude\` per farlo." else ok 'Claude: accesso presente' fi if [[ $VPS == 0 ]]; then local TSBIN TSBIN=$(tailscale_bin || true) if [[ $PROVA == 1 ]] || ! "$TSBIN" status >/dev/null 2>&1; then if [[ $SISTEMA == Darwin ]]; then # Su macOS l'accesso si fa dall'app, dall'icona nella barra dei menu. ok 'Collega questo computer a Tailscale: si apre l'"'"'app, accedi dall'"'"'icona nella barra dei menu.' fai 'open -a Tailscale' || attenzione 'Tailscale non si è aperto: aprilo dalle Applicazioni.' if [[ $PROVA == 0 && $TERMINALE == 1 ]]; then read -r -p ' Premi Invio quando hai fatto l'"'"'accesso. ' _ Labelapp.nomadev.server ProgramArguments$(xml_val "$BIN")start EnvironmentVariables PATH$(xml_val "$PATH") NOMADEV_HOME$(xml_val "$NOMADEV_HOME") RunAtLoadKeepAlive StandardOutPath$(xml_val "$NOMADEV_HOME/server.log") StandardErrorPath$(xml_val "$NOMADEV_HOME/server.log") " # bootout ferma anche il server di un'installazione precedente; launchd # finisce di toglierlo poco dopo, da qui la pausa prima di bootstrap. if fai "mkdir -p '$NOMADEV_HOME'; launchctl bootout $DOMINIO/app.nomadev.server 2>/dev/null; sleep 1; launchctl bootstrap $DOMINIO '$PLIST'"; then [[ $PROVA == 1 ]] || ok 'Agente launchd app.nomadev.server attivo' else attenzione 'Avvio automatico non attivato. Per avviare il server a mano: nomadev start' fi else # Servizio utente, non di sistema: chiavi SSH, configurazione di Claude e # progetti stanno nella home dell'utente. # Il PATH scritto qui è quello di adesso, al momento dell'installazione: # se Node cambia posto (es. nvm passa a un'altra versione) basta # rilanciare l'installer, che riscrive il file. Vale anche per il plist. # Restart=always: il server su SIGTERM esce con 0, che on-failure non riavvia. scrivi_file "$HOME/.config/systemd/user/nomadev.service" "[Unit] Description=NomaDev server After=network-online.target [Service] ExecStart=\"$(systemd_val "$BIN" exec)\" start Environment=\"PATH=$(systemd_val "$PATH")\" Environment=\"NOMADEV_HOME=$(systemd_val "$NOMADEV_HOME")\" Restart=always RestartSec=5 [Install] WantedBy=default.target" # Sul VPS nessuno resta collegato: senza linger il servizio muore con la # sessione SSH. Prima del servizio, perché è il linger che tiene acceso il # systemd dell'utente. if [[ $VPS == 1 ]]; then fai "${S}loginctl enable-linger $(id -un)" || attenzione 'Linger non attivato: il server si fermerà quando esci dalla sessione SSH.' fi # Entrati con su o sudo -i questa variabile manca, e systemctl --user non # trova il systemd dell'utente. export XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/run/user/$(id -u)} # restart e non solo --now: rilanciando lo script dopo un aggiornamento il # servizio acceso deve ripartire con la versione nuova. if fai 'systemctl --user daemon-reload && systemctl --user enable nomadev.service && systemctl --user restart nomadev.service'; then [[ $PROVA == 1 ]] || ok 'Servizio systemd utente nomadev.service attivo' else attenzione 'Avvio automatico non attivato (systemd utente non disponibile?). Per avviare il server a mano: nomadev start' fi fi # Nome nella lingua del sistema; trattino ASCII perché finisce in un nome di file. local NOME_ICONA='NomaDev - Connect phone' case "${LC_ALL:-${LANG:-}}" in it*) NOME_ICONA='NomaDev - Collega telefono' ;; esac if [[ $VPS == 0 ]]; then passo 'Icona per collegare il telefono' # Un clic e si apre la pagina con il QR. collega.sh fissa PATH e # cartella di adesso, come il servizio: se Node cambia posto, si # rilancia l'installer. local LANCIA="$NOMADEV_HOME/collega.sh" scrivi_file "$LANCIA" "#!/bin/sh # Scritto dall'installer di NomaDev: apre la pagina con il QR per collegare il telefono. export PATH=$(sh_val "$PATH") export NOMADEV_HOME=$(sh_val "$NOMADEV_HOME") exec $(sh_val "$BIN") pair" fai "chmod 700 '$LANCIA'" if [[ $SISTEMA == Darwin ]]; then local APP="$HOME/Applications/$NOME_ICONA.app" if fai "mkdir -p '$HOME/Applications' && rm -rf '$APP' && osacompile -o '$APP' -e 'do shell script quoted form of \"$LANCIA\"'"; then [[ $PROVA == 1 ]] || ok "App \"$NOME_ICONA\" in ~/Applications" else attenzione 'App non creata: per collegare un telefono usa nomadev pair' fi else scrivi_file "$HOME/.local/share/applications/nomadev-collega.desktop" "[Desktop Entry] Type=Application Name=NomaDev – Connect phone Name[it]=NomaDev – Collega telefono Comment=Opens the page with the QR code to connect your phone Comment[it]=Apre la pagina con il QR per collegare il telefono Exec=\"$(desktop_val "$LANCIA")\" Icon=smartphone Terminal=false Categories=Utility;" [[ $PROVA == 1 ]] || ok "Voce \"$NOME_ICONA\" nel menu delle applicazioni" fi fi passo 'Collega il telefono' fai 'nomadev pair' || attenzione 'Codice non generato: riprova con nomadev pair' if [[ $VPS == 1 ]]; then # I passi per intero: il QR qui sopra punta a 127.0.0.1 finché non c'è # urlPubblico, e dal telefono non si raggiunge. attenzione 'Sul VPS il server ascolta solo in locale (127.0.0.1:4000): non esporre la porta.' attenzione 'Metti davanti un reverse proxy con TLS. Con Caddy, in /etc/caddy/Caddyfile:' attenzione ' tuo.dominio { reverse_proxy 127.0.0.1:4000 }' attenzione "Poi in $NOMADEV_HOME/config.json aggiungi \"urlPubblico\": \"https://tuo.dominio\"" attenzione 'e rilancia nomadev pair per il QR giusto.' fi if [[ $PROVA == 0 ]]; then [[ $LOCALE_NEL_PATH == 1 ]] || attenzione "Se in un nuovo terminale \`nomadev\` non si trova, aggiungi $HOME/.local/bin al PATH." if [[ $VPS == 1 ]]; then printf '\nFatto. Per collegare un altro telefono in futuro: nomadev pair\n\n' else printf '\nFatto. Per collegare un altro telefono: apri "%s" (oppure nomadev pair).\n\n' "$NOME_ICONA" fi fi } principale "$@"