Files
goclaw/cmd/gateway_packages_wiring.go
Duy /zuey/ 425cecb9a3 feat(packages): Phase 2b — apk update flow + pkg-helper v2 protocol (#900) (#7)
* 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
2026-05-11 15:41:27 +07:00

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)
}