2026-10-15 11:49:22 +02:00
using System.Net.Http.Headers ;
using System.Text.Json ;
using System.Text.RegularExpressions ;
namespace AgentWikiUi.Core ;
enum InstallMethod { Native , Npm , Homebrew , WinGet }
/// <summary>Esito del controllo (sola lettura): versione installata (riga di `--version`), ultima pubblicata e se è più nuova. Available null = sconosciuto.</summary>
record UpdateCheck ( string? InstalledText , string? Latest , bool? Available );
/// <summary>Esito di un aggiornamento: messaggio da mostrare e, se riuscito, le versioni prima e dopo.</summary>
record UpdateOutcome ( bool Ok , string Message , string? Before , string? After );
// Aggiornamento di un CLI con il suo comando ufficiale (`<cli> update`). Vedi ADR-0004.
static class EngineUpdater
{
public static readonly TimeSpan UpdateTimeout = TimeSpan . FromMinutes ( 10 );
static readonly TimeSpan VersionTimeout = TimeSpan . FromSeconds ( 30 );
static readonly TimeSpan CheckTimeout = TimeSpan . FromSeconds ( 10 );
static readonly HttpClient SharedHttp = NewHttp ();
static HttpClient NewHttp ()
{
var http = new HttpClient { Timeout = CheckTimeout };
http . DefaultRequestHeaders . UserAgent . Add ( new ProductInfoHeaderValue ( AppInfo . Name , "1.0" )); // GitHub lo richiede
return http ;
}
/// <summary>Primo numero N.N.N del testo ("2.1.300 (Claude Code)", "codex-cli 0.162.1", "rust-v0.162.1").</summary>
public static Version ? ParseVersion ( string? text )
{
var m = text is null ? null : Regex . Match ( text , @"\d+\.\d+\.\d+" );
return m is { Success : true } && Version . TryParse ( m . Value , out var v ) ? v : null ;
}
/// <summary>true se <paramref name="latest"/> è più nuova di <paramref name="installed"/>; null se una delle due non si legge.</summary>
public static bool? IsNewer ( string? latest , string? installed ) =>
ParseVersion ( latest ) is { } l && ParseVersion ( installed ) is { } i ? l > i : null ;
/// <summary>Controlla se c'è una versione più nuova senza installare nulla. Mai eccezioni: rete o formato inattesi ⇒ sconosciuto.</summary>
public static async Task < UpdateCheck > CheckAsync ( Engine engine , HttpClient ? http = null )
{
var installed = await GetVersionAsync ( engine );
string? latest = null ;
try
{
using var cts = new CancellationTokenSource ( CheckTimeout );
latest = await engine . FetchLatestVersionAsync ( http ?? SharedHttp , cts . Token );
}
catch ( Exception ex ) when ( ex is HttpRequestException or TaskCanceledException or OperationCanceledException or JsonException or KeyNotFoundException or InvalidOperationException ) { }
return new ( installed , latest , IsNewer ( latest , installed ));
}
/// <summary>Prima riga di `--version` ripulita; null se l'eseguibile manca o fallisce.</summary>
public static async Task < string? > GetVersionAsync ( Engine engine , TimeSpan ? timeout = null )
{
try
{
var r = await EngineRunner . RunToolAsync ( engine , [ "--version" ], timeout ?? VersionTimeout );
if ( r . TimedOut || r . Code != 0 ) return null ;
return r . Out . Split ( '\n' , StringSplitOptions . RemoveEmptyEntries | StringSplitOptions . TrimEntries ). FirstOrDefault ();
}
catch ( System . ComponentModel . Win32Exception ) { return null ; }
}
public static async Task < UpdateOutcome > UpdateAsync ( Engine engine , TimeSpan ? timeout = null )
{
var before = await GetVersionAsync ( engine );
( int Code , string Out , string Err , bool TimedOut ) r ;
try { r = await EngineRunner . RunToolAsync ( engine , [ "update" ], timeout ?? UpdateTimeout ); }
catch ( System . ComponentModel . Win32Exception )
{
return new ( false , $"'{engine.Executable}' non trovato nel PATH." , before , null );
}
var manual = ManualCommand ( engine , DetectMethod ( PathSearch . Find ( engine . Executable )));
if ( r . TimedOut ) return new ( false , $"Aggiornamento interrotto: più di {Elapsed(timeout ?? UpdateTimeout)}.\nComando manuale: {manual}" , before , null );
if ( r . Code != 0 ) return new ( false , $"Aggiornamento non riuscito (codice {r.Code}).\n{Tail(r.Err, r.Out)}\nComando manuale: {manual}" . Replace ( "\n\n" , "\n" ), before , null );
var after = await GetVersionAsync ( engine );
if ( before is not null && after is not null && before != after ) return new ( true , $"Aggiornato da {before} a {after}." , before , after );
return new ( true , $"Già aggiornato ({after ?? before ?? " versione sconosciuta "})." , before , after );
}
static string Elapsed ( TimeSpan t ) => t . TotalMinutes >= 1 ? $"{t.TotalMinutes:0} minuti" : $"{t.TotalSeconds:0} secondi" ;
// Ultime righe utili dell'output (stderr se c'è, altrimenti stdout).
static string Tail ( string err , string output )
{
var text = string . IsNullOrWhiteSpace ( err ) ? output : err ;
return string . Join ( '\n' , text . Split ( '\n' , StringSplitOptions . RemoveEmptyEntries | StringSplitOptions . TrimEntries ). TakeLast ( 4 ));
}
/// <summary>Metodo di installazione dedotto dal percorso dell'eseguibile, anche dopo aver risolto i collegamenti simbolici.</summary>
public static InstallMethod DetectMethod ( string? path )
{
if ( path is null ) return InstallMethod . Native ;
foreach ( var p in new [] { path , RealPath ( path ) })
{
if ( Has ( p , "node_modules" ) || Has ( p , ".npm" )) return InstallMethod . Npm ;
if ( Has ( p , "Cellar" ) || Has ( p , "homebrew" ) || Has ( p , "linuxbrew" )) return InstallMethod . Homebrew ;
if ( Has ( p , "WinGet" ) || Has ( p , "WindowsApps" )) return InstallMethod . WinGet ;
}
return InstallMethod . Native ;
}
static bool Has ( string path , string part ) => path . Contains ( part , StringComparison . OrdinalIgnoreCase );
static string RealPath ( string path )
{
try { return new FileInfo ( path ). ResolveLinkTarget ( returnFinalTarget : true )?. FullName ?? path ; }
catch ( IOException ) { return path ; }
}
/// <summary>Comando da eseguire a mano quando l'aggiornamento automatico non basta (tabella dell'ADR-0004).</summary>
public static string ManualCommand ( Engine engine , InstallMethod method ) => ( engine . Id , method ) switch
{
( "claude" , InstallMethod . Npm ) => "npm install -g @anthropic-ai/claude-code@latest" ,
( "claude" , InstallMethod . Homebrew ) => "brew upgrade claude-code" ,
( "claude" , InstallMethod . WinGet ) => "winget upgrade Anthropic.ClaudeCode" ,
( "claude" , _ ) => "claude update" ,
( "codex" , InstallMethod . Npm ) => "npm install -g @openai/codex@latest" ,
( "codex" , InstallMethod . Homebrew ) => "brew upgrade --cask codex" ,
( "codex" , _ ) => OperatingSystem . IsWindows ()
? "powershell -ExecutionPolicy ByPass -c \"irm https://chatgpt.com/codex/install.ps1 | iex\""
: "curl -fsSL https://chatgpt.com/codex/install.sh | sh" ,
2026-10-18 14:41:29 +02:00
( "agy" , InstallMethod . Homebrew ) => "brew upgrade agy" ,
( "agy" , _ ) => "agy update" ,
2026-10-15 11:49:22 +02:00
_ => $"{engine.Executable} update" ,
};
}