mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-13 00:14:33 +00:00
* feat(packages): add apk update flow + pkg-helper v2 protocol - APK update checker/executor via helper IPC (runtime detection, upgrade scan via apk list --upgradable) - BREAKING: pkg-helper v2 protocol (5 actions: check_apk/check_pip/check_npm/exec_apk/exec_pip, code/data fields, renewable 10min deadline, apkMutex, 1MB scanner) - Edition gating: SupportsApk + IsAlpineRuntime double-gate (Standard/Full only) - Backend 3-branch wiring: alpine/apt/yum routes + update_registry, dep_installer helpers - i18n: 5 apk keys (EN/VI/ZH catalogs) - Frontend: source pill Alpine badge, APK in updates-list/summary-bar/update-all modal - E2E tests: apk_e2e build tag covering checker/executor/helper protocol - Docs: packages-apk.md, security/changelog updates - Plans + reports under plans/260417-1500-packages-update-phase2b-apk-pkghelper/ + plans/reports/ * docs(packages): journal Phase 2b apk + pkg-helper v2
88 lines
3.6 KiB
Go
88 lines
3.6 KiB
Go
package cmd
|
|
|
|
import (
|
|
"log/slog"
|
|
"path/filepath"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/edition"
|
|
httpapi "github.com/nextlevelbuilder/goclaw/internal/http"
|
|
"github.com/nextlevelbuilder/goclaw/internal/skills"
|
|
)
|
|
|
|
// wirePackagesHandler constructs the UpdateRegistry and wires it into
|
|
// PackagesHandler together with the gateway's event publisher.
|
|
//
|
|
// Called after initGitHubInstaller() so DefaultGitHubInstaller() is non-nil.
|
|
// If the installer is not configured (e.g. in integration-test stubs), returns
|
|
// a handler with nil registry — the update endpoints return 503.
|
|
func wirePackagesHandler(d *gatewayDeps) *httpapi.PackagesHandler {
|
|
installer := skills.DefaultGitHubInstaller()
|
|
if installer == nil {
|
|
slog.Warn("packages: github installer not configured; update endpoints disabled")
|
|
return httpapi.NewPackagesHandler(nil, d.msgBus)
|
|
}
|
|
|
|
// Cache file lives next to the manifest dir so it shares the same atomic-
|
|
// write guarantees on the same filesystem (no cross-device rename risk).
|
|
cachePath := filepath.Join(filepath.Dir(installer.Config.ManifestPath), "updates-cache.json")
|
|
|
|
cache, err := skills.LoadUpdateCache(cachePath)
|
|
if err != nil {
|
|
// ErrUpdateCacheCorrupt — log and proceed with an empty cache; a
|
|
// background refresh will repopulate on first GET /v1/packages/updates.
|
|
slog.Warn("packages: update cache corrupt; starting fresh", "path", cachePath, "error", err)
|
|
}
|
|
|
|
ttl := d.cfg.Packages.UpdatesCheckTTLDuration()
|
|
registry := skills.NewUpdateRegistry(cache, cachePath, ttl)
|
|
|
|
// Share the installer's locker so Install and Update share per-package locks.
|
|
registry.Locker = installer.Locker
|
|
skills.SetSharedPackageLocker(registry.Locker)
|
|
|
|
// Register checker + executor for "github" source.
|
|
registry.RegisterChecker(skills.NewGitHubUpdateChecker(installer))
|
|
|
|
executor := skills.NewGitHubUpdateExecutor(installer)
|
|
if d.cfg.Packages.ScratchDir != "" {
|
|
executor.ScratchDir = d.cfg.Packages.ScratchDir
|
|
}
|
|
registry.RegisterExecutor(executor)
|
|
|
|
// Register pip + npm checkers/executors when the edition supports them.
|
|
if edition.Current().SupportsPipNpm {
|
|
registry.RegisterChecker(skills.NewPipUpdateChecker())
|
|
registry.RegisterExecutor(skills.NewPipUpdateExecutor())
|
|
registry.RegisterChecker(skills.NewNpmUpdateChecker())
|
|
registry.RegisterExecutor(skills.NewNpmUpdateExecutor())
|
|
}
|
|
|
|
// Register apk checker/executor when edition + runtime both permit.
|
|
// Double gate: edition flag (compile-time) + /etc/alpine-release (runtime).
|
|
// Rationale: Standard-Debian variants pass the edition gate but fail runtime;
|
|
// Lite on Alpine fails the edition gate but passes runtime. Both must hold.
|
|
if edition.Current().SupportsApk && skills.IsAlpineRuntime() {
|
|
registry.RegisterChecker(skills.NewApkUpdateChecker())
|
|
registry.RegisterExecutor(skills.NewApkUpdateExecutor())
|
|
slog.Info("packages: apk updates registered")
|
|
} else if edition.Current().SupportsApk {
|
|
// Standard edition but non-Alpine host: emit explicit availability=false
|
|
// so frontend can distinguish "not applicable to this runtime" from
|
|
// "checker errored". Lite skips both branches → availability.apk absent.
|
|
registry.SetAvailability("apk", false)
|
|
slog.Info("packages: apk updates skipped (non-Alpine runtime)",
|
|
"is_alpine_runtime", skills.IsAlpineRuntime())
|
|
} else {
|
|
// Lite edition: no registration, no availability seed (key absent in response).
|
|
slog.Info("packages: apk updates skipped (edition does not support apk)")
|
|
}
|
|
|
|
slog.Info("packages: update registry wired",
|
|
"cache", cachePath,
|
|
"ttl", ttl,
|
|
"sources", registry.Sources(),
|
|
)
|
|
|
|
return httpapi.NewPackagesHandler(registry, d.msgBus)
|
|
}
|