# Installa NomaDev su Windows. # # irm https://nomadev.app/install.ps1 | iex # # Per ogni programma che manca chiede prima di installarlo. Senza domande: # $env:NOMADEV_SI=1; irm https://nomadev.app/install.ps1 | iex # Prova senza fare niente: $env:NOMADEV_PROVA=1 (o -Prova se lanciato da file). # Rilanciarlo e' sicuro: salta quello che c'e' gia' e non tocca la configurazione. # # Scritto per Windows PowerShell 5.1, quello che c'e' su ogni Windows: niente # sintassi solo di PowerShell 7 (&&, ??, operatore ternario). # # Solo caratteri ASCII, accenti compresi (e', a'), e niente BOM: `irm` decodifica # secondo il charset che manda il server, e senza charset legge Latin-1. Un BOM # o una lettera accentata in UTF-8 diventerebbero spazzatura, e il BOM davanti # a `param` rompe lo script intero. Vale anche per -File, che senza BOM legge # nella codepage ANSI. param([switch]$Si, [switch]$Prova) # Tutto dentro un blocco: con `irm | iex` lo script gira nella sessione di chi # lo lancia, e funzioni e preferenze non devono restargli addosso. & { param([bool]$Si, [bool]$Prova) $ErrorActionPreference = 'Stop' if ($env:NOMADEV_SI -eq '1') { $Si = $true } if ($env:NOMADEV_PROVA -eq '1') { $Prova = $true } # Lo stesso nome che usa `nomadev update` (server/src/aggiorna.ts). $NOME_ATTIVITA = 'NomaDev server' $FERMATO = 'nomadev-installazione-fermata' # L'esito dell'ultimo Fai (vedi sotto): in una tabella e non in una variabile # di script, che con `irm | iex` finirebbe nella sessione di chi lo lancia. $stato = @{ Riuscito = $false } function Passo($t) { Write-Host "`n== $t" -ForegroundColor Cyan } function Ok($t) { Write-Host " $t" -ForegroundColor Green } function Attenzione($t) { Write-Host " ! $t" -ForegroundColor Yellow } # Niente `exit`: lanciato con `irm | iex` chiuderebbe la finestra di chi lo usa, # messaggio compreso. Si interrompe con un'eccezione che il fondo dello script # riconosce. function Fermati($t) { Write-Host "`n X $t`n" -ForegroundColor Red; throw $FERMATO } function Presente($bin) { [bool](Get-Command $bin -ErrorAction SilentlyContinue) } # Esegue un comando, o lo stampa in modalita' prova. L'esito va in # $stato.Riuscito e non nel valore restituito: catturare l'uscita della # funzione vorrebbe dire catturare quella del comando, e il QR di # `nomadev pair`, il link di `tailscale up` e la schermata di `claude` # sparirebbero invece di arrivare a schermo. function Fai([string]$comando) { if ($Prova) { Write-Host " [prova] $comando"; $stato.Riuscito = $true; return } $global:LASTEXITCODE = 0 try { Invoke-Expression $comando $stato.Riuscito = ($LASTEXITCODE -eq 0 -or $null -eq $LASTEXITCODE) } catch { Write-Host " $($_.Exception.Message)" -ForegroundColor Red $stato.Riuscito = $false } } # Vero se il comando riesce; non stampa niente. Su PowerShell 5.1 un # programma che scrive su stderr mentre lo si zittisce diventa un errore, che # con 'Stop' fermerebbe tutto: qui un errore vuol dire solo "no". function RiesceZitto([scriptblock]$blocco) { $ErrorActionPreference = 'Continue' $global:LASTEXITCODE = 0 try { & $blocco *> $null; return ($LASTEXITCODE -eq 0) } catch { return $false } } # Un testo dentro apici singoli di PowerShell: l'apice si raddoppia. function Apici([string]$t) { $t -replace "'", "''" } function Chiedi([string]$domanda) { if ($Si -or $Prova) { return $true } $r = Read-Host " $domanda [S/n]" return ($r -eq '' -or $r -match '^[sSyY]') } # Dopo un'installazione il PATH della sessione e' quello vecchio: senza # rileggerlo il programma appena installato risulterebbe ancora mancante. # ~/.local/bin e' dove si installa Claude Code. function AggiornaPath { $env:Path = [Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [Environment]::GetEnvironmentVariable('Path', 'User') + ';' + (Join-Path $HOME '.local\bin') } function Installa([string]$nome, [string]$bin, [string]$comando, [string]$link) { if (-not $Prova -and (Presente $bin)) { Ok "${nome}: presente"; return } if (-not $comando) { Fermati "$nome manca e qui non so installarlo. Installalo da $link e rilancia questo comando." } if (-not (Chiedi "Manca $nome. Lo installo?")) { Fermati "$nome serve. Installalo da $link e rilancia questo comando." } Write-Host " Installo $nome" Fai $comando if (-not $stato.Riuscito) { Fermati "Installazione di $nome non riuscita. Installalo da $link e rilancia." } if (-not $Prova) { AggiornaPath if (-not (Presente $bin)) { Fermati "$nome installato ma non ancora visibile. Chiudi e riapri PowerShell, poi rilancia questo comando." } } } # Un server gia' acceso (rilancio dello script) tiene bloccati i file del # pacchetto e npm non riuscirebbe a sostituirli: lo si ferma prima. Come # `nomadev update`: schtasks /End chiude il PowerShell dell'attivita', ma il # node figlio puo' restare vivo; lo si trova dal pid che il server scrive e, # se e' davvero un node, lo si ferma e si aspetta che sparisca. function FermaServer { if ($Prova) { Write-Host " [prova] schtasks /End /TN `"$NOME_ATTIVITA`" e taskkill del node in server.pid, se acceso"; return } # Disattivata finche' npm non ha finito: la ripetizione ogni 5 minuti la # farebbe ripartire con i file ancora da sostituire. Si riattiva da sola # rifacendo l'attivita', o nel catch se lo script si ferma prima. RiesceZitto { schtasks /Change /TN $NOME_ATTIVITA /DISABLE } | Out-Null RiesceZitto { schtasks /End /TN $NOME_ATTIVITA } | Out-Null $filePid = Join-Path $env:NOMADEV_HOME 'server.pid' if (-not (Test-Path $filePid)) { return } $id = 0 if (-not [int]::TryParse(([string](Get-Content $filePid -Raw)).Trim(), [ref]$id)) { return } $p = Get-Process -Id $id -ErrorAction SilentlyContinue if (-not $p -or $p.ProcessName -ne 'node') { return } RiesceZitto { taskkill /PID $id /T /F } | Out-Null try { Wait-Process -Id $id -Timeout 10 -ErrorAction SilentlyContinue } catch { } } # Le installazioni fatte con i vecchi script del repo (install/windows.ps1) # registravano l'attivita' "FreeDev server", che lancia `npm start` dalla # cartella server del repo. Accesa insieme a quella nuova si contenderebbe la # porta 4000, e il nuovo server non partirebbe. Prima di registrare la nuova si # propone di toglierla; in prova si mostra sempre, perche' si veda cosa farebbe. # Come per FermaServer, fermare l'attivita' chiude il PowerShell ma il node # figlio puo' restare vivo con la porta. Non lo si cerca dalla porta: la 4000 # e' comune tra i server di sviluppo, e si chiuderebbe un programma che non # c'entra. Si fermano solo i node che lanciano src/index.ts dalla cartella # scritta nell'azione della vecchia attivita' (`cd ""; npm start`), # letta prima di toglierla. Se non la si riconosce non si ferma niente. function CartellaVecchiaAttivita([string]$nome) { try { foreach ($a in (Get-ScheduledTask -TaskName $nome -ErrorAction Stop).Actions) { if ([string]$a.Arguments -match 'cd\s+"([^"]+)"') { return $Matches[1].TrimEnd('\', '/') } } } catch { } return $null } function FermaNodeVecchio([string]$cartella) { # Col separatore in fondo: C:\repo\server non deve valere per C:\repo\server2. $forme = @("$cartella\", ("$cartella/" -replace '\\', '/')) $fermati = 0 try { foreach ($p in (Get-CimInstance Win32_Process -Filter "Name='node.exe'" -ErrorAction Stop)) { $riga = [string]$p.CommandLine if ($riga -notmatch 'src[\\/]index\.ts') { continue } $suo = $false foreach ($f in $forme) { if ($riga.IndexOf($f, [StringComparison]::OrdinalIgnoreCase) -ge 0) { $suo = $true } } if (-not $suo) { continue } RiesceZitto { taskkill /PID $p.ProcessId /T /F } | Out-Null $fermati++ } } catch { } return $fermati } function TogliVecchiaAttivita { $vecchia = 'FreeDev server' if ($Prova) { Ok "(se c'e' l'attivita' `"$vecchia`" di una vecchia installazione dal repo)" } elseif (-not (RiesceZitto { schtasks /Query /TN $vecchia })) { return } Attenzione "C'e' l'attivita' `"$vecchia`" di una vecchia installazione dal repo: avvia il server con npm start e terrebbe occupata la porta 4000, cosi' il nuovo non partirebbe." if (-not (Chiedi 'La fermo e la tolgo?')) { Attenzione "Lasciata. Finche' e' accesa il nuovo server non parte: toglila da Utilita' di pianificazione." return } if ($Prova) { $cartella = $null } else { $cartella = CartellaVecchiaAttivita $vecchia } Fai "Stop-ScheduledTask -TaskName '$vecchia' -ErrorAction SilentlyContinue; Unregister-ScheduledTask -TaskName '$vecchia' -Confirm:`$false" if (-not $stato.Riuscito) { Attenzione "Non sono riuscito a toglierla: fallo da Utilita' di pianificazione."; return } if ($Prova) { Write-Host " [prova] fermo solo i node con src/index.ts della cartella scritta nella vecchia attivita' (mai per porta)" } elseif ($cartella) { $n = FermaNodeVecchio $cartella if ($n) { Ok "Fermato il server rimasto acceso dalla vecchia attivita' ($cartella)." } } else { Attenzione "Non riconosco la cartella della vecchia attivita': non fermo nessun processo. Se la porta 4000 resta occupata, riavvia il computer." } Ok "Per tenere i telefoni gia' collegati: copia config.json e la cartella data dalla cartella server del vecchio repo in $env:NOMADEV_HOME, poi rilancia questo comando." } try { # Con la politica predefinita di Windows PowerShell i comandi installati # da npm (npm.ps1, nomadev.ps1) non partono. Vale solo per questa # finestra; se un criterio di gruppo lo vieta si va avanti lo stesso. try { Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force } catch { } if (-not $Prova) { AggiornaPath } # La configurazione sta sempre li', qualunque sia la cartella da cui si # lancia lo script o da cui parte il server: con una config.json nella # cartella corrente `nomadev` userebbe quella (vedi percorsi.ts). Chi ha # gia' scelto NOMADEV_HOME tiene la sua. Vale per init e pair qui sotto # e per l'attivita' pianificata. if (-not $env:NOMADEV_HOME) { $env:NOMADEV_HOME = Join-Path $HOME '.nomadev' } Passo 'Programmi necessari' $winget = Presente 'winget' $W = { param($id) if ($winget -or $Prova) { "winget install --id $id -e --accept-source-agreements --accept-package-agreements" } else { '' } } Installa 'Node.js' 'node' (& $W 'OpenJS.NodeJS.LTS') 'https://nodejs.org' if (-not $Prova) { $maggiore = [int]((& node -v) -replace '^v(\d+).*', '$1') if ($maggiore -lt 20) { Fermati "Node.js $(& node -v) troppo vecchio: serve almeno la 20. Aggiornalo da https://nodejs.org e rilancia." } } Installa 'git' 'git' (& $W 'Git.Git') 'https://git-scm.com/download/win' # In un PowerShell a parte: un `exit` dentro l'installer di Claude non deve # chiudere questo. Installa 'Claude Code' 'claude' 'powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://claude.ai/install.ps1 | iex"' 'https://claude.ai/download' Installa 'Tailscale' 'tailscale' (& $W 'Tailscale.Tailscale') 'https://tailscale.com/download/windows' Passo 'Accessi' # Claude Code su Windows tiene le credenziali in ~/.claude/.credentials.json. if ($Prova -or -not (Test-Path (Join-Path $HOME '.claude\.credentials.json'))) { Ok "Fai l'accesso a Claude con il tuo abbonamento (si apre il browser), poi esci con /exit." Fai 'claude' } else { Ok 'Claude: accesso presente' } if ($Prova -or -not (RiesceZitto { tailscale status })) { Ok 'Collega questo computer a Tailscale (si apre il browser).' Fai 'tailscale up' if (-not $stato.Riuscito) { Attenzione 'Tailscale non collegato: il telefono lo raggiunge solo dalla rete di casa.' } } else { Ok 'Tailscale: collegato' } Passo 'NomaDev' FermaServer Fai 'npm install -g nomadev' if (-not $stato.Riuscito) { # Come `nomadev update`: il server fermato per installare riparte, # meglio la versione vecchia accesa che nessun server. if (-not $Prova) { RiesceZitto { schtasks /Change /TN $NOME_ATTIVITA /ENABLE } | Out-Null; RiesceZitto { schtasks /Run /TN $NOME_ATTIVITA } | Out-Null } Fermati 'Installazione di NomaDev non riuscita (npm). Rilancia questo comando; se si ripete, scrivici il messaggio qui sopra.' } if (-not $Prova) { AggiornaPath if (-not (Presente 'nomadev')) { Fermati 'NomaDev installato ma il comando nomadev non si trova. Chiudi e riapri PowerShell, poi rilancia questo comando.' } } Fai 'nomadev init' if (-not $stato.Riuscito) { Fermati 'Configurazione di NomaDev non riuscita: vedi il messaggio qui sopra.' } Passo 'Avvio automatico al login' TogliVecchiaAttivita # Gira come l'utente vero, non come SYSTEM: chiavi SSH, configurazione di # Claude e progetti stanno nella sua home. PowerShell nascosto, cosi' al # login non resta aperta nessuna finestra; Bypass perche' nomadev.ps1, # il comando che npm crea, parta anche con la politica predefinita. # ExecutionTimeLimit zero: altrimenti Windows lo ferma dopo 72 ore. # nomadev col percorso completo, non dal PATH: al login il PATH puo' # non essere ancora quello di questa finestra. NOMADEV_HOME impostata # prima dell'avvio, come sopra. # Due trigger: all'accesso, e uno a orario ripetuto ogni 5 minuti. Se il # server gira la ripetizione non fa niente (IgnoreNew), se e' stato chiuso # riparte. La ripetizione dentro il trigger all'accesso non basta: parte # solo dopo un accesso vero, non dopo l'installazione. if ($Prova) { $bin = Join-Path $env:APPDATA 'npm\nomadev.ps1' } else { $bin = (Get-Command nomadev).Source } $argomento = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -Command `"`$env:NOMADEV_HOME='$(Apici $env:NOMADEV_HOME)'; & '$(Apici $bin)' start`"" $utente = "$env:USERDOMAIN\$env:USERNAME" $registra = @" `$azione = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '$(Apici $argomento)' `$trigger = New-ScheduledTaskTrigger -AtLogOn -User '$(Apici $utente)' `$ripeti = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) -RepetitionInterval (New-TimeSpan -Minutes 5) `$imp = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit ([TimeSpan]::Zero) -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) Unregister-ScheduledTask -TaskName '$NOME_ATTIVITA' -Confirm:`$false -ErrorAction SilentlyContinue Register-ScheduledTask -TaskName '$NOME_ATTIVITA' -Action `$azione -Trigger @(`$trigger, `$ripeti) -Settings `$imp -Description 'Server NomaDev' | Out-Null Start-ScheduledTask -TaskName '$NOME_ATTIVITA' "@ if ($Prova) { Write-Host " [prova] Register-ScheduledTask `"$NOME_ATTIVITA`" (nomadev start al login, nascosta):" foreach ($riga in ($registra.Trim() -split "`r?`n")) { Write-Host " [prova] $riga" } } else { try { Invoke-Expression $registra Ok "Avvio automatico registrato e acceso (`"$NOME_ATTIVITA`" in Utilita' di pianificazione)" } catch { Attenzione "Avvio automatico non registrato: $($_.Exception.Message)" Attenzione 'Per avviare il server a mano: nomadev start' } } Passo 'Icona per collegare il telefono' # Un clic e si apre la pagina con il QR: il cliente non deve ricordarsi # nessun comando. Stesso PowerShell nascosto dell'attivita', nome nella # lingua del sistema. Solo ASCII: anche il trattino del nome. if ((Get-Culture).TwoLetterISOLanguageName -eq 'it') { $nomeIcona = 'NomaDev - Collega telefono' } else { $nomeIcona = 'NomaDev - Connect phone' } $argIcona = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -Command `"`$env:NOMADEV_HOME='$(Apici $env:NOMADEV_HOME)'; & '$(Apici $bin)' pair`"" $psExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' $iconeFatte = 0 foreach ($cartella in @([Environment]::GetFolderPath('Programs'), [Environment]::GetFolderPath('Desktop'))) { # Una cartella che il profilo non ha (Desktop reindirizzato o tolto) non # deve fermare l'installazione: si salta, l'altra icona basta. if (-not $cartella) { continue } $lnk = Join-Path $cartella "$nomeIcona.lnk" if ($Prova) { Write-Host " [prova] collegamento $lnk -> powershell.exe $argIcona"; continue } try { $c = (New-Object -ComObject WScript.Shell).CreateShortcut($lnk) $c.TargetPath = $psExe $c.Arguments = $argIcona $c.WindowStyle = 7 $c.Description = 'Apre la pagina con il QR per collegare il telefono' $c.Save() $iconeFatte++ } catch { Attenzione "Icona non creata in ${cartella}: $($_.Exception.Message)" } } if ($iconeFatte -gt 0) { Ok "Icona `"$nomeIcona`" nel menu Start e sul desktop" } Passo 'Collega il telefono' Fai 'nomadev pair' if (-not $Prova) { Write-Host "`nFatto. Per collegare un altro telefono: apri `"$nomeIcona`" dal menu Start o dal desktop (oppure nomadev pair).`n" -ForegroundColor Cyan } } catch { # Fermati dopo FermaServer lascerebbe l'attivita' disattivata: il server # non ripartirebbe piu', nemmeno al login. if (-not $Prova) { RiesceZitto { schtasks /Change /TN $NOME_ATTIVITA /ENABLE } | Out-Null } if ($_.Exception.Message -ne $FERMATO) { Write-Host "`n X Errore inatteso: $($_.Exception.Message)`n" -ForegroundColor Red } # Da file si esce con un codice d'errore; con `irm | iex` ci si ferma e # basta, la finestra resta aperta col messaggio. if ($PSCommandPath) { exit 1 } } } ([bool]$Si) ([bool]$Prova)