297 lines
8.5 KiB
Nim
297 lines
8.5 KiB
Nim
## update_checker.nim
|
|
## Automatic update checking for NIP, recipes, and tools
|
|
|
|
import std/[os, times, json, httpclient, strutils, options, tables, osproc]
|
|
|
|
type
|
|
UpdateChannel* = enum
|
|
Stable = "stable"
|
|
Beta = "beta"
|
|
Nightly = "nightly"
|
|
|
|
UpdateFrequency* = enum
|
|
Never = "never"
|
|
Daily = "daily"
|
|
Weekly = "weekly"
|
|
Monthly = "monthly"
|
|
|
|
UpdateConfig* = object
|
|
enabled*: bool
|
|
channel*: UpdateChannel
|
|
frequency*: UpdateFrequency
|
|
lastCheck*: Time
|
|
notifyRecipes*: bool
|
|
notifyTools*: bool
|
|
notifyNip*: bool
|
|
|
|
UpdateInfo* = object
|
|
component*: string # "recipes", "nix", "gentoo", "nip"
|
|
currentVersion*: string
|
|
latestVersion*: string
|
|
updateAvailable*: bool
|
|
changelog*: string
|
|
downloadUrl*: string
|
|
|
|
UpdateChecker* = ref object
|
|
config*: UpdateConfig
|
|
configPath*: string
|
|
cacheDir*: string
|
|
|
|
const
|
|
DefaultUpdateUrl = "https://updates.nip.example.com/v1"
|
|
DefaultConfigPath = ".config/nip/update-config.json"
|
|
|
|
proc getConfigPath*(): string =
|
|
## Get update config path
|
|
let xdgConfig = getEnv("XDG_CONFIG_HOME", getHomeDir() / ".config")
|
|
result = xdgConfig / "nip" / "update-config.json"
|
|
|
|
proc loadConfig*(path: string = ""): UpdateConfig =
|
|
## Load update configuration
|
|
result = UpdateConfig()
|
|
result.enabled = true
|
|
result.channel = Stable
|
|
result.frequency = Weekly
|
|
result.lastCheck = fromUnix(0)
|
|
result.notifyRecipes = true
|
|
result.notifyTools = true
|
|
result.notifyNip = true
|
|
|
|
var configPath = path
|
|
if configPath.len == 0:
|
|
configPath = getConfigPath()
|
|
|
|
if not fileExists(configPath):
|
|
return
|
|
|
|
try:
|
|
let data = readFile(configPath)
|
|
let config = parseJson(data)
|
|
|
|
if config.hasKey("enabled"):
|
|
result.enabled = config["enabled"].getBool()
|
|
|
|
if config.hasKey("channel"):
|
|
let channelStr = config["channel"].getStr()
|
|
case channelStr
|
|
of "stable": result.channel = Stable
|
|
of "beta": result.channel = Beta
|
|
of "nightly": result.channel = Nightly
|
|
else: discard
|
|
|
|
if config.hasKey("frequency"):
|
|
let freqStr = config["frequency"].getStr()
|
|
case freqStr
|
|
of "never": result.frequency = Never
|
|
of "daily": result.frequency = Daily
|
|
of "weekly": result.frequency = Weekly
|
|
of "monthly": result.frequency = Monthly
|
|
else: discard
|
|
|
|
if config.hasKey("lastCheck"):
|
|
result.lastCheck = fromUnix(config["lastCheck"].getInt())
|
|
|
|
if config.hasKey("notifyRecipes"):
|
|
result.notifyRecipes = config["notifyRecipes"].getBool()
|
|
|
|
if config.hasKey("notifyTools"):
|
|
result.notifyTools = config["notifyTools"].getBool()
|
|
|
|
if config.hasKey("notifyNip"):
|
|
result.notifyNip = config["notifyNip"].getBool()
|
|
|
|
except:
|
|
echo "Warning: Failed to load update config: ", getCurrentExceptionMsg()
|
|
|
|
proc saveConfig*(config: UpdateConfig, path: string = "") =
|
|
## Save update configuration
|
|
var configPath = path
|
|
if configPath.len == 0:
|
|
configPath = getConfigPath()
|
|
|
|
# Create config directory
|
|
createDir(configPath.parentDir())
|
|
|
|
var configJson = newJObject()
|
|
configJson["enabled"] = %config.enabled
|
|
configJson["channel"] = %($config.channel)
|
|
configJson["frequency"] = %($config.frequency)
|
|
configJson["lastCheck"] = %config.lastCheck.toUnix()
|
|
configJson["notifyRecipes"] = %config.notifyRecipes
|
|
configJson["notifyTools"] = %config.notifyTools
|
|
configJson["notifyNip"] = %config.notifyNip
|
|
|
|
writeFile(configPath, $configJson)
|
|
|
|
proc newUpdateChecker*(config: UpdateConfig = loadConfig()): UpdateChecker =
|
|
## Create a new update checker
|
|
result = UpdateChecker()
|
|
result.config = config
|
|
result.configPath = getConfigPath()
|
|
|
|
let xdgCache = getEnv("XDG_CACHE_HOME", getHomeDir() / ".cache")
|
|
result.cacheDir = xdgCache / "nip" / "updates"
|
|
createDir(result.cacheDir)
|
|
|
|
proc shouldCheck*(uc: UpdateChecker): bool =
|
|
## Check if we should check for updates based on frequency
|
|
if not uc.config.enabled:
|
|
return false
|
|
|
|
if uc.config.frequency == Never:
|
|
return false
|
|
|
|
let now = getTime()
|
|
let timeSinceLastCheck = now - uc.config.lastCheck
|
|
|
|
case uc.config.frequency
|
|
of Never:
|
|
return false
|
|
of Daily:
|
|
return timeSinceLastCheck.inDays >= 1
|
|
of Weekly:
|
|
return timeSinceLastCheck.inDays >= 7
|
|
of Monthly:
|
|
return timeSinceLastCheck.inDays >= 30
|
|
|
|
proc checkRecipeUpdates*(uc: UpdateChecker): Option[UpdateInfo] =
|
|
## Check for recipe repository updates
|
|
if not uc.config.notifyRecipes:
|
|
return none(UpdateInfo)
|
|
|
|
try:
|
|
# Check Git repository for updates
|
|
let recipesDir = getEnv("XDG_DATA_HOME", getHomeDir() / ".local/share") / "nip" / "recipes"
|
|
|
|
if not dirExists(recipesDir / ".git"):
|
|
return none(UpdateInfo)
|
|
|
|
# Get current commit
|
|
let currentCommit = execProcess("git -C " & recipesDir & " rev-parse HEAD").strip()
|
|
|
|
# Fetch latest
|
|
discard execProcess("git -C " & recipesDir & " fetch origin main 2>&1")
|
|
|
|
# Get latest commit
|
|
let latestCommit = execProcess("git -C " & recipesDir & " rev-parse origin/main").strip()
|
|
|
|
if currentCommit != latestCommit:
|
|
# Get changelog
|
|
let changelog = execProcess("git -C " & recipesDir & " log --oneline " & currentCommit & ".." & latestCommit).strip()
|
|
|
|
var info = UpdateInfo()
|
|
info.component = "recipes"
|
|
info.currentVersion = currentCommit[0..7]
|
|
info.latestVersion = latestCommit[0..7]
|
|
info.updateAvailable = true
|
|
info.changelog = changelog
|
|
return some(info)
|
|
|
|
except:
|
|
discard
|
|
|
|
return none(UpdateInfo)
|
|
|
|
proc checkToolUpdates*(uc: UpdateChecker, toolName: string): Option[UpdateInfo] =
|
|
## Check for tool updates (Nix, Gentoo, PKGSRC)
|
|
if not uc.config.notifyTools:
|
|
return none(UpdateInfo)
|
|
|
|
# For now, tools are updated via recipes
|
|
# This could be extended to check tool-specific update mechanisms
|
|
return none(UpdateInfo)
|
|
|
|
proc checkNipUpdates*(uc: UpdateChecker): Option[UpdateInfo] =
|
|
## Check for NIP updates
|
|
if not uc.config.notifyNip:
|
|
return none(UpdateInfo)
|
|
|
|
try:
|
|
let client = newHttpClient()
|
|
let url = DefaultUpdateUrl & "/nip/latest?channel=" & $uc.config.channel
|
|
|
|
let response = client.get(url)
|
|
|
|
if response.code == Http200:
|
|
let data = parseJson(response.body)
|
|
|
|
if data.hasKey("version"):
|
|
let latestVersion = data["version"].getStr()
|
|
let currentVersion = "0.1.0" # TODO: Get from build info
|
|
|
|
if latestVersion != currentVersion:
|
|
var info = UpdateInfo()
|
|
info.component = "nip"
|
|
info.currentVersion = currentVersion
|
|
info.latestVersion = latestVersion
|
|
info.updateAvailable = true
|
|
|
|
if data.hasKey("changelog"):
|
|
info.changelog = data["changelog"].getStr()
|
|
|
|
if data.hasKey("downloadUrl"):
|
|
info.downloadUrl = data["downloadUrl"].getStr()
|
|
|
|
return some(info)
|
|
|
|
except:
|
|
discard
|
|
|
|
return none(UpdateInfo)
|
|
|
|
proc checkAllUpdates*(uc: UpdateChecker): seq[UpdateInfo] =
|
|
## Check for all available updates
|
|
result = @[]
|
|
|
|
# Check recipes
|
|
let recipeUpdate = uc.checkRecipeUpdates()
|
|
if recipeUpdate.isSome:
|
|
result.add(recipeUpdate.get())
|
|
|
|
# Check NIP
|
|
let nipUpdate = uc.checkNipUpdates()
|
|
if nipUpdate.isSome:
|
|
result.add(nipUpdate.get())
|
|
|
|
# Update last check time
|
|
uc.config.lastCheck = getTime()
|
|
saveConfig(uc.config)
|
|
|
|
proc formatUpdateNotification*(info: UpdateInfo): string =
|
|
## Format update notification for display
|
|
result = "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
|
result.add("📦 Update Available: " & info.component & "\n")
|
|
result.add("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n")
|
|
result.add("Current Version: " & info.currentVersion & "\n")
|
|
result.add("Latest Version: " & info.latestVersion & "\n")
|
|
|
|
if info.changelog.len > 0:
|
|
result.add("\nChangelog:\n")
|
|
result.add(info.changelog & "\n")
|
|
|
|
result.add("\nTo update, run:\n")
|
|
case info.component
|
|
of "recipes":
|
|
result.add(" nip update recipes\n")
|
|
of "nip":
|
|
result.add(" nip update self\n")
|
|
else:
|
|
result.add(" nip update " & info.component & "\n")
|
|
|
|
proc showUpdateNotifications*(updates: seq[UpdateInfo], quiet: bool = false) =
|
|
## Show update notifications to user
|
|
if updates.len == 0:
|
|
if not quiet:
|
|
echo "✅ All components are up to date"
|
|
return
|
|
|
|
echo ""
|
|
for update in updates:
|
|
echo formatUpdateNotification(update)
|
|
echo ""
|
|
|
|
if updates.len > 1:
|
|
echo "To update all components:"
|
|
echo " nip update --all"
|
|
echo ""
|