Files
agent-wiki-ui/Core/EngineUpdater.cs
T
davideandClaude Sonnet 5.5 8b20f5224e Impostazioni: controllo e aggiornamento dei CLI con pulsante evidenziato
Sezione Aggiornamenti nel riquadro Impostazioni: per ogni CLI installato
versione, esito del controllo (disponibile, aggiornato, non riuscito) e
pulsante Aggiorna che esegue il comando ufficiale. Il pulsante e un puntino
sul pulsante Impostazioni si evidenziano solo se c'è una versione nuova. Il
controllo legge l'ultima versione dalle sorgenti ufficiali (downloads.claude.ai
e release GitHub di Codex), parte all'avvio e all'apertura del riquadro con
cache di 5 minuti e non fallisce mai in modo invasivo. L'aggiornamento ha
tempo massimo di 10 minuti, blocca invio e cambio CLI, e in caso di errore
suggerisce il comando manuale per npm, Homebrew, WinGet o installazione nativa.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 11:49:22 +02:00

132 lines
6.8 KiB
C#

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",
_ => $"{engine.Executable} update",
};
}