Documentation
¶
Overview ¶
Package render orchestrates the apply pipeline: canonical model + adapter registry -> per-agent FileOps + Skips. apply flag controls whether ops are written to disk or returned for inspection (e.g. --dry-run).
Package render — translation report.
TranslationReport summarises, per plugin (or source component), how many items each adapter rendered vs skipped. Emitted at the end of apply/verify.
Index ¶
- Constants
- func BackupFile(home, path string) (string, error)
- func CollectPointers(m map[string]any, prefix string) []string
- func IsKeyMerge(strategy string) bool
- func OrphanFiles(s *state.Targets, userHome, agent string, scope adapter.Scope, project string, ...) []string
- func PruneBackups(home string, keep int) error
- func PruneStaleState(s *state.Targets, userHome, agent string, scope adapter.Scope, project string, ...)
- func RecordOpsState(s *state.Targets, userHome, agent string, scope adapter.Scope, project string, ...) error
- type AgentResult
- type CollisionReport
- func Apply(p RenderPlan, reg *adapter.Registry, st *state.Targets, home string, ...) ([]CollisionReport, map[string]bool, map[string]bool, error)
- func PreviewApply(p RenderPlan, reg *adapter.Registry, st *state.Targets, home string, ...) (reports []CollisionReport, unchanged, wouldChange map[string]bool, err error)
- type PluginRow
- type RenderPlan
- type TranslationReport
- type Writer
Constants ¶
const DefaultBackupKeep = 20
DefaultBackupKeep is how many backup-timestamp dirs apply retains.
Variables ¶
This section is empty.
Functions ¶
func BackupFile ¶
BackupFile copies the file at path into <home>/.state/backups/<ts>/ verbatim (0600) and returns the backup path. It is the standalone analog of the apply Writer's per-collision backup, for callers (reconcile's interactive orphan delete) that must preserve a file before a destructive action so no choice loses content. Returns ("", nil) when path is missing.
func CollectPointers ¶
CollectPointers walks m and returns JSON pointers for every leaf-or-object at the second level (e.g. /mcpServers/github → stop). agentsync owns at the second-level granularity; deeper edits fall under that key's value hash. The prefix argument is used for recursive calls; callers should pass "".
func IsKeyMerge ¶
IsKeyMerge reports whether a MergeStrategy accumulates pointers into a shared file (rather than replacing the whole file). Such ops must never be deduped by path — one agent emits several of them to the same destination. The rendered op.Content is always JSON regardless of the on-disk format (TOML for merge-toml-keys); only the destination file is decoded per strategy via decodeDestObject.
func OrphanFiles ¶
func OrphanFiles(s *state.Targets, userHome, agent string, scope adapter.Scope, project string, ops []adapter.FileOp) []string
OrphanFiles returns the absolute dest paths that agent+scope+project still OWNS in state as whole-file (replace-strategy) entries but the current plan's ops no longer render — i.e. the source component was removed since the last apply. The same detection PruneStaleState uses, surfaced so diagnostics (status/diff/reconcile) can report a dest the next apply would prune instead of falsely reporting "clean". Returns absolute paths, sorted.
func PruneBackups ¶
PruneBackups removes all but the most recent `keep` timestamp directories under <home>/.state/backups. Each backup is a verbatim copy of a pre-existing native config file (which may contain secrets), so they must not accumulate unbounded — a disk-bloat and credential-lingering concern. The dir names are zero-padded, fixed-width timestamps, so lexical sort is chronological. Best-effort: a removal error for one dir doesn't abort.
func PruneStaleState ¶
func PruneStaleState(s *state.Targets, userHome, agent string, scope adapter.Scope, project string, ops []adapter.FileOp)
PruneStaleState removes Files / Keys entries owned by agent+scope+project whose path or pointer is no longer produced by the current set of ops. This must be called BEFORE RecordOpsState so the freshly-applied entries don't get pruned by their own absence in the previous run's state.
Without this, removing an MCP server / skill / hook from ~/.agentsync/ leaves its state entry behind forever; it shows up as `Orphan` in `status` and `targets.json` grows unbounded over time.
The userHome argument (the user's $HOME, paths.HomeDir) is used to normalize op.Path to its HOME-relative form so state-key lookups match keys written by RecordOpsState. It must NOT be the agentsync home — dest files live under $HOME, not under ~/.agentsync.
func RecordOpsState ¶
func RecordOpsState(s *state.Targets, userHome, agent string, scope adapter.Scope, project string, ops []adapter.FileOp) error
RecordOpsState updates s with hashes for files and keys produced by ops. Caller is expected to call this AFTER a successful Apply.
Both op.Path and project are normalized to HOME-relative form via paths.HomeRelative (against userHome, the user's $HOME) before being embedded in the state-map keys, so the resulting targets.json is portable across machines whose $HOME differs (e.g. /Users/alice/ vs /home/alice/ after a chezmoi sync). userHome must NOT be the agentsync home: dest files live under $HOME, not under ~/.agentsync, so using the agentsync home as the base leaves every key machine-absolute and unportable.
Types ¶
type CollisionReport ¶
type CollisionReport struct {
Agent string
Path string
Pointer string // empty for whole-file collisions
BackupTo string // absolute path of the backup that was written
}
CollisionReport describes one foreign-collision the writer detected and backed up. Callers can surface these to the user.
func Apply ¶
func Apply( p RenderPlan, reg *adapter.Registry, st *state.Targets, home string, userHome string, scope adapter.Scope, project string, ) ([]CollisionReport, map[string]bool, map[string]bool, error)
Apply commits a RenderPlan by constructing one Writer per agent and invoking adapter.Apply with that writer. The writer enforces the foreign-collision backup invariant on every destination write — there is no separate "guard" pass; the guarantee is intrinsic to the only write path adapters are permitted to use.
Returns the union of CollisionReports across all agents so the caller can surface them, plus the set of destination paths actually written this run (so the apply-error rescue can record state for exactly those files and no others). If any adapter returns an error, applies completed so far are NOT rolled back (each underlying iox.AtomicWrite is atomic per-file, but the plan as a whole is not transactional); the reports and written-set returned reflect the work done before the failure.
Deduplication: when two adapters emit a "write" op for the same path (e.g. a shared skill file written by both claude and opencode), the first one wins and the second is silently skipped. Content is deterministic per path, so skipping a duplicate is always safe.
func PreviewApply ¶
func PreviewApply( p RenderPlan, reg *adapter.Registry, st *state.Targets, home string, userHome string, scope adapter.Scope, project string, ) (reports []CollisionReport, unchanged, wouldChange map[string]bool, err error)
PreviewApply runs the apply pipeline through non-writing preview writers and reports what a real apply would do, without touching disk. It returns the foreign-collision reports a real apply would produce, plus the per-destination convergence verdict: `unchanged` paths already hold exactly the bytes apply would write (the dry-run shows them as "synced"), and `wouldChange` paths would be created or modified (shown as "write"). Used by `apply --dry-run`.
It supersedes the older op.Content-only collision preview: because it runs the real adapter Apply, each merge is performed for real (against the on-disk destination), so the preview's synced/write verdict and collision set match the eventual apply exactly rather than approximating it.
func (CollisionReport) String ¶
func (r CollisionReport) String() string
String formats a CollisionReport for human output.
type PluginRow ¶
type PluginRow struct {
Plugin string `json:"plugin"`
Agent string `json:"agent"`
// Coverage is "full", "partial", or "none".
Coverage string `json:"coverage"`
// MCP is the number of MCP servers rendered for this plugin×agent pair.
MCP int `json:"mcp"`
// Commands is the number of slash commands rendered.
Commands int `json:"commands"`
// Skips is the number of components the adapter explicitly skipped.
Skips int `json:"skips"`
// Disabled is true when the plugin is disabled for this scope (e.g. by a
// project marker's [plugins] disabled). Its components are not rendered.
Disabled bool `json:"disabled,omitempty"`
}
PluginRow is one row in the translation report — one plugin × one agent.
type RenderPlan ¶
type RenderPlan struct {
PerAgent map[string]AgentResult
}
RenderPlan holds the result of rendering a canonical model through every selected adapter. PerAgent[name] is the per-agent breakdown.
func Plan ¶
func Plan(r secrets.Resolved, reg *adapter.Registry, agents []string, scope adapter.Scope, project string, s *state.Targets, userHome string) (RenderPlan, error)
Plan asks each adapter named in agents to render the canonical model. Returns a RenderPlan, never writes anything. Use Apply() to commit.
s may be nil; when non-nil, OwnedKeys is populated on merge-json-keys ops from state.Keys so the apply pipeline knows which JSON-pointer paths it owns.
userHome (the user's $HOME, paths.HomeDir) is required so OwnedKeys lookups match the HOME-relative form state stores. It is NOT the agentsync home — dest files live under $HOME, not under ~/.agentsync.
func (RenderPlan) Total ¶
func (p RenderPlan) Total() int
Total returns the total number of FileOps across all agents.
type TranslationReport ¶
type TranslationReport struct {
Rows []PluginRow `json:"rows"`
}
TranslationReport holds all plugin×agent rows.
func BuildReport ¶
func BuildReport(c source.Canonical, plan RenderPlan, agents []string) TranslationReport
BuildReport constructs a TranslationReport from the canonical model and a RenderPlan. It associates ops/skips with plugins by matching MCP server IDs contributed by each plugin.
For each plugin P and each agent A:
- count MCP servers that appear in plan.PerAgent[A].Ops whose SourceID matches one of P's components.
- coverage = full if skips==0 && mcp>0 (or no components), partial if skips>0 && mcp>0, none if mcp==0.
Plugins with no canonical entries (not yet cached) still generate rows with coverage=none so the operator can see what's missing.
func (TranslationReport) PrintJSON ¶
func (r TranslationReport) PrintJSON(w io.Writer) error
PrintJSON writes the structured JSON form.
func (TranslationReport) PrintText ¶
func (r TranslationReport) PrintText(w io.Writer)
PrintText writes the human-readable report to w in its plain form.
plugin: demo@test-mp claude ✓ full (1 mcp, 0 commands) opencode ✓ full (1 mcp, 0 commands)
Use PrintTextStyled to render the same report with semantic color via a *ui.Printer. Plain output is byte-stable so existing test fixtures hold.
func (TranslationReport) PrintTextStyled ¶
func (r TranslationReport) PrintTextStyled(w io.Writer, p *ui.Printer)
PrintTextStyled writes the report with the same layout as PrintText, but styled via p: bold "plugin:" labels, semantically colored coverage marks (green=full, yellow=partial, red=none), and faint trailing counts. When p has color disabled (non-TTY, NO_COLOR, --color=never), the output is visually identical to PrintText — only ANSI is suppressed.
type Writer ¶
type Writer struct {
// contains filtered or unexported fields
}
Writer is THE funnel for native-destination writes. Every adapter's Apply method receives one of these and routes its writes through it (rather than calling iox.AtomicWrite directly), so the foreign-collision backup invariant is enforced at the only place it matters: the moment of the write.
Pre-write, Writer checks the (agent, scope, project, path) tuple against state. If state does not yet record this destination as owned AND the file already exists with content that differs from what we are about to write, the existing content is copied to <home>/.state/backups/<ts>/<original-path> before the new write proceeds. For merge ops, the same check runs at JSON-pointer granularity using op.Content (ours pre-merge) and op.OwnedKeys.
The forbidigo lint rule in .golangci.yml fails any direct iox.AtomicWrite call outside the allowed packages, so a future contributor cannot silently bypass this guard.
func NewPreviewWriter ¶
func NewPreviewWriter(st *state.Targets, home, userHome string, scope adapter.Scope, project, agent string) *Writer
NewPreviewWriter constructs a Writer that records foreign-collision reports without performing any disk writes. Used by `apply --dry-run` to surface the same backup-and-overwrite events a real apply would produce.
func NewWriter ¶
func NewWriter(st *state.Targets, home, userHome string, scope adapter.Scope, project, agent string) *Writer
NewWriter constructs a Writer for one (agent, scope, project) tuple. The render layer creates one writer per agent during Apply. home is the agentsync home (backup root); userHome is the user's $HOME (state-key normalization base).
func (*Writer) Delete ¶
Delete satisfies adapter.DestWriter. Idempotent on missing files.
Skill-orphan deletes (op.SourceID under "skills/", synthesized by Apply when a skill or bundled file is removed from source) get two extra guarantees: a dest that drifted from what agentsync last wrote is backed up before removal (the never-destroy-unsynced-content invariant the write path enforces), and empty skill directories left behind are pruned up to — but never including — the agent's skills root. Other delete callers (agent disable --purge, reconcile orphan removal) pass an empty SourceID and keep the plain idempotent remove.
func (*Writer) Reports ¶
func (w *Writer) Reports() []CollisionReport
Reports returns the per-write collision reports accumulated so far. Safe to call after Apply completes.
func (*Writer) Unchanged ¶
Unchanged returns the set of destination paths that already held exactly the bytes apply would write, so the atomic write (and its mtime churn) was skipped. apply uses it to report "up to date" instead of "applied: N ops".
func (*Writer) WouldChange ¶
WouldChange returns the set of destination paths a dry-run write would have created or modified (the complement of Unchanged for the preview). It is only populated when the writer is in dry-run mode; on a real apply it is empty because the write is actually performed. `apply --dry-run` uses it to label each planned destination "write" vs "synced".
func (*Writer) Write ¶
Write satisfies adapter.DestWriter. finalBytes is the post-merge content for merge ops, or op.Content for replace ops.
func (*Writer) Wrote ¶
Wrote returns the set of destination paths this writer actually wrote. Used by the apply-error rescue to record state ONLY for files agentsync committed this run — a pre-existing foreign file at an op that was never attempted must not be recorded as owned (that would suppress its backup).