package client import ( "fmt" "io" "os" "time" "github.com/m3tam3re/agent-lib/internal/deploy" "github.com/m3tam3re/agent-lib/internal/gitsource" "github.com/m3tam3re/agent-lib/internal/lockfile" "github.com/m3tam3re/agent-lib/internal/target" ) // SyncReport summarizes one planner-driven sync run. type SyncReport struct { Revision string Deployed int Updated int Kept int Removed int Skipped int Left int Manifest string } // ExecutePlan applies a plan item by item: deploys and removes are atomic // per item, every skip+warn and leave+warn is appended to the warning log // with item name and reason, and the manifest is rewritten once at the end. func ExecutePlan(tree *gitsource.GitTree, plan []PlanEntry, manifest *Manifest, paths StatePaths, rev string, stdout io.Writer) (*SyncReport, error) { report := &SyncReport{Revision: rev, Manifest: paths.Manifest} var warnings []string for _, e := range plan { key := manifestKey(e.Type, e.Name) switch e.Action { case ActDeploy, ActUpdate: if err := deployEntry(tree, e); err != nil { return nil, fmt.Errorf("%s %s/%s: %w", e.Action, e.Type, e.Name, err) } manifest.Items[key] = &ManifestItem{ Name: e.Name, Type: e.Type, Origin: e.Origin, Revision: rev, Hash: e.NewHash, } if e.Action == ActDeploy { report.Deployed++ } else { report.Updated++ } case ActKeep: report.Kept++ case ActRemove: if err := os.RemoveAll(e.DstPath); err != nil { return nil, fmt.Errorf("removing %s/%s: %w", e.Type, e.Name, err) } delete(manifest.Items, key) report.Removed++ case ActSkip: warnings = append(warnings, fmt.Sprintf("SKIP %s/%s: %s", e.Type, e.Name, e.Reason)) report.Skipped++ case ActLeave: warnings = append(warnings, fmt.Sprintf("LEAVE %s/%s: %s", e.Type, e.Name, e.Reason)) report.Left++ } } manifest.Version = ManifestVersion manifest.Revision = rev if err := manifest.Save(paths.Manifest); err != nil { return nil, err } if len(warnings) > 0 { if err := appendWarnings(paths.LogFile, warnings); err != nil { fmt.Fprintf(stdout, "warning: could not write sync log: %v\n", err) } } printPlannedSyncReport(stdout, report) return report, nil } func deployEntry(tree *gitsource.GitTree, e PlanEntry) error { if e.Type == lockfile.TypeSkill { return deploy.Dir(tree, e.RelPath, e.DstPath) } return deploy.File(tree, e.RelPath, e.DstPath) } // appendWarnings adds timestamped warning lines to the sync log. func appendWarnings(logPath string, warnings []string) error { f, err := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) if err != nil { return err } defer f.Close() stamp := time.Now().UTC().Format(time.RFC3339) for _, w := range warnings { if _, err := fmt.Fprintf(f, "%s %s\n", stamp, w); err != nil { return err } } return nil } // diskFacts observes every deployed path the planner can care about: // manifest items, current inventory items, and unmanaged occupants of the // target directories. func diskFacts(oc *target.OpenCode, manifest *Manifest, items []PlannedItem) map[string]DiskFact { facts := map[string]DiskFact{} observe := func(typ, name string) { key := manifestKey(typ, name) if _, seen := facts[key]; seen { return } path, ok := oc.DeployPath(typ, name) if !ok { return } if _, err := os.Stat(path); err != nil { facts[key] = DiskFact{} return } hash, err := HashDiskPath(typ, path) if err != nil { hash = "" } facts[key] = DiskFact{Exists: true, Hash: hash} } for _, rec := range manifest.Items { observe(rec.Type, rec.Name) } for _, it := range items { observe(it.Type, it.Name) } for key := range oc.ScanTargetDirs() { if _, seen := facts[key]; !seen { facts[key] = DiskFact{Exists: true} } } return facts } func printPlannedSyncReport(w io.Writer, r *SyncReport) { fmt.Fprintf(w, "synced to revision %s\n", shortRev(r.Revision)) fmt.Fprintf(w, " deployed: %d\n", r.Deployed) fmt.Fprintf(w, " updated: %d\n", r.Updated) fmt.Fprintf(w, " kept: %d\n", r.Kept) fmt.Fprintf(w, " removed: %d\n", r.Removed) if r.Skipped > 0 { fmt.Fprintf(w, " skipped (local modifications kept): %d\n", r.Skipped) } if r.Left > 0 { fmt.Fprintf(w, " unmanaged items left untouched: %d\n", r.Left) } fmt.Fprintf(w, " manifest: %s\n", r.Manifest) } func shortRev(rev string) string { if len(rev) > 12 { return rev[:12] } return rev }