sync

package
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 30, 2026 License: MIT Imports: 31 Imported by: 0

Documentation

Overview

Package sync orchestrates materialization (spec §7.5, §13.11) for both registry sources: a filesystem registry (walk the local layers) and a server registry (read the effective view over the §7.5 HTTP API). Both run the configured HarnessAdapter and write atomically through pkg/materialize. The source is chosen per §7.5.2 dispatch: an http(s) URL routes to the server, every other value to the filesystem.

Index

Constants

View Source
const (
	KindWorkspace   = "workspace"
	KindMarketplace = "marketplace"
)

Target kinds (§7.5.2). The empty value defaults to KindWorkspace.

Variables

View Source
var (
	// ErrProfileNotFound signals that the named profile is missing
	// from sync.yaml.
	ErrProfileNotFound = errors.New("override: profile not found")
	// ErrInvalidArtifactID signals an empty or malformed --add /
	// --remove argument.
	ErrInvalidArtifactID = errors.New("override: invalid artifact id")
)

Errors related to override / save-as / profile edit.

View Source
var (
	// ErrNoTarget signals that Options.Target was empty.
	ErrNoTarget = errors.New("sync: target directory not specified")
	// ErrNoRegistry signals that no registry source was configured: Run
	// returns it when Options.RegistryPath is empty. Maps to
	// config.no_registry in §6.10. Resolving the registry from CLI flags,
	// PODIUM_REGISTRY, or a discovered sync.yaml (the §7.5.2 precedence
	// chain) is the caller's responsibility; pkg/sync reads no environment
	// variables or config files.
	ErrNoRegistry = errors.New("config.no_registry: no registry configured")
	// ErrOfflineCacheMiss signals a §7.4 offline-only sync against a
	// server-source registry. podium sync keeps no offline content cache, so
	// offline-only ("never contact the registry; structured error if cache
	// miss") has nothing local to materialize. The code lives in the §6.10
	// network.* namespace, matching the MCP server.
	ErrOfflineCacheMiss = errors.New("network.offline_cache_miss: offline-only mode cannot reach a server-source registry and podium sync keeps no offline cache")
	// ErrRegistryUnreachable signals a §7.4 always-revalidate sync (the
	// default mode) against an unreachable server-source registry.
	// always-revalidate returns "structured error network.registry_unreachable"
	// when there is no cache; podium sync keeps no offline content cache, so an
	// unreachable server-source registry has nothing to serve and surfaces this
	// namespaced §6.10 code rather than the raw transport message. offline-first
	// is the silent no-op above; offline-only returns ErrOfflineCacheMiss.
	ErrRegistryUnreachable = errors.New("network.registry_unreachable: always-revalidate mode cannot reach the server-source registry and podium sync keeps no offline cache")
)

Errors returned by Run. Tests assert against them via errors.Is.

View Source
var ErrConfigInvalid = errors.New("config.invalid")

ErrConfigInvalid signals a marketplace target that fails validation: a harness set naming a non-publish-target harness, a malformed plugin glob, or a malformed workflow command. It maps to config.invalid in §6.10. Callers assert against it via errors.Is.

View Source
var (
	// ErrConfigNotFound signals that no sync.yaml was found in any
	// configured scope. Maps to config.no_registry in §6.10 when it
	// surfaces during a sync.
	ErrConfigNotFound = errors.New("config.not_found")
)

Errors related to config files.

View Source
var ErrUnbalancedBraces = errors.New("unbalanced braces")

ErrUnbalancedBraces signals a glob pattern with mismatched "{" / "}".

Functions

func Check added in v0.1.3

func Check(merged *MergedConfig) []string

Check validates a merged config per §7.5.2 `podium sync --check` and returns the warnings (never errors): unresolved profile references (defaults.profile and per-target profile names), malformed include/exclude globs, duplicate target ids, and profile-name collisions across scopes. The returned slice is sorted for deterministic output and is empty when the config is clean.

spec: §7.5.2 — "validate the merged config against the schema and report unresolved profile references, malformed globs, target collisions, and profile-name collisions across scopes (warning, not error)".

func ConfigPath

func ConfigPath(workspace string) string

ConfigPath returns the canonical path to a workspace's sync.yaml.

func DiscoverWorkspace added in v0.1.3

func DiscoverWorkspace(start string) (string, bool)

DiscoverWorkspace walks up from start until it finds a directory containing a `.podium/` subdirectory, mirroring how git locates `.git` (§7.5.2). It returns the directory holding `.podium` and true, or ("", false) when none is found before the filesystem root.

func IsServerSource added in v0.1.3

func IsServerSource(registry string) bool

isServerSource reports whether registry resolves to a Podium server under the §7.1 / §7.5.2 dispatch: an http:// or https:// URL routes to a server, and every other value (a bare path, a file:// URI) is a filesystem source. Run and Watch use it to reject a server URL with a canonical error instead of letting filesystem.Open mangle the URL into a bogus path under the working directory.

spec: §7.1, §7.5.2 — "a URL routes to a Podium server, a filesystem path routes to local filesystem". IsServerSource is the exported form of isServerSource for callers outside the package (the CLI uses it to decide whether the §3.5 scope-preview endpoint is reachable over HTTP).

func LockFilePath

func LockFilePath(target string) string

LockFilePath returns the canonical lock-file path for a target directory.

func MergeKindForOp added in v0.2.0

func MergeKindForOp(op adapter.FileOp) string

MergeKindForOp maps an adapter.FileOp to the lock's config-merge kind string ("json", "inject", or "" for a standalone write), so the marketplace render records the same Merge value in its lock that sync does and Reconcile treats the path identically on the next render.

func PathSignature added in v0.1.3

func PathSignature(paths ...string) string

PathSignature returns a stable content fingerprint over one or more filesystem trees, suitable for poll-based change detection. Two signatures are equal iff every (path, modtime, size) tuple matches across calls. It is exported so other poll-based watchers (the §6.4 MCP overlay watcher) reuse the same detection logic.

func PriorMergeKinds added in v0.2.0

func PriorMergeKinds(lock *LockFile) map[string]string

PriorMergeKinds returns the materialized paths a lock recorded, each mapped to its §6.7 config-merge kind, for the Reconcile prior argument. A nil lock returns an empty map.

func Reconcile added in v0.2.0

func Reconcile(target string, prior map[string]string, current map[string]bool)

Reconcile removes from target every materialized path the prior lock recorded that the current render did not write, reusing the §7.5 stale-file cleanup: a standalone path is deleted and its empty parents pruned, and a §6.7 config-merge path (the PodiumOwnedKey JSON manifests, the inject blocks) is reconciled in place so the operator's other entries survive. It is the exported form of the internal sync cleanup so the marketplace render reconciles a marketplace tree on re-render through the same code path `podium sync` uses, giving an idempotent re-render and stale-file removal (§7.8 reconciliation).

prior maps each prior materialized path to its config-merge kind ("json", "inject", or "" for a standalone file), as PriorMergeKinds returns from a lock. current is the set of paths the render wrote this time.

func ResolveRegistryPath

func ResolveRegistryPath(workspace, registry string) string

ResolveRegistryPath resolves a sync.yaml `defaults.registry` value per §13.11.2: filesystem URLs return as-is, absolute paths return as-is, and relative paths resolve against the workspace. Empty input returns "".

func ValidateGlob added in v0.2.0

func ValidateGlob(pattern string) error

ValidateGlob reports whether pattern is a well-formed §7.5.1 glob. It is the exported form of validateGlob for callers outside the package. The marketplace output validation reuses it to validate a kind: marketplace target's plugin scope-filter globs (§7.8) against the same rules `podium sync --check` applies.

func ValidateOutput added in v0.2.0

func ValidateOutput(out ResolvedOutput) error

ValidateOutput rejects a marketplace target whose harness set names a non-publish-target harness (opencode, none, or an unknown id), a target with a malformed plugin glob, and a target with a malformed workflow command. All map to config.invalid (§6.10). The harness check reuses the §7.8 publish-target selector (adapter.EmitterForHarness), the glob check reuses the §7.5.1 sync glob validator (ValidateGlob), and the command check reuses the per-step validation (Command.validate), so config validation and the render path agree on which harnesses publish, which globs are well-formed, and which commands are well-formed. Validating the workflow commands here makes --check fail-closed: a command that declares neither run: nor sh:, or both, is rejected before the prepare clone or the render runs (§7.5.2 "--check validates the config only").

func Watch

func Watch(ctx context.Context, opts WatchOptions) (<-chan WatchEvent, error)

Watch runs an initial sync, then polls the registry and overlay paths and reruns Run on every change. Stops when ctx is canceled, returning ctx.Err() (typically context.Canceled).

Each rerun emits one WatchEvent on the returned channel. The channel is closed when the watcher exits.

func WriteConfig

func WriteConfig(workspace string, cfg *SyncConfig) error

WriteConfig writes the SyncConfig back to the workspace's sync.yaml atomically via .tmp + rename.

func WriteLock

func WriteLock(target string, lf *LockFile) error

WriteLock writes the lock file atomically (`.tmp` + rename) so readers see either the previous or the new content.

Types

type ArtifactResult

type ArtifactResult struct {
	ID          string
	Version     string
	Type        string
	ContentHash string
	Layer       string
	Files       []string
}

ArtifactResult is one artifact's contribution to the materialized output. Version and Type come from the parsed manifest (empty when the frontmatter did not parse) and feed the §7.5 --json artifact entries.

type Command added in v0.2.0

type Command struct {
	Run             []string `yaml:"run,omitempty"`
	Sh              string   `yaml:"sh,omitempty"`
	SkipIfNoChanges bool     `yaml:"skip_if_no_changes,omitempty"`
	ContinueOnError bool     `yaml:"continue_on_error,omitempty"`
	Timeout         Duration `yaml:"timeout,omitempty"`
}

Command is one step of a workflow phase (§7.8). It is an argv list under `run:` executed directly without a shell, or a string under `sh:` executed through `sh -c`. The per-command flags control failure handling: SkipIfNoChanges skips the command when the render produced no diff, ContinueOnError lets the pipeline proceed past a non-zero exit, and Timeout bounds the command's wall-clock duration.

type Defaults

type Defaults struct {
	Registry string `yaml:"registry,omitempty"`
	Harness  string `yaml:"harness,omitempty"`
	Target   string `yaml:"target,omitempty"`
	Profile  string `yaml:"profile,omitempty"`
	// Identity is the §4.6 effective-view principal a `kind: marketplace`
	// target inherits when it declares none of its own (§7.5.2). It is the
	// publishing identity whose visibility defines what reaches a public
	// marketplace; a `kind: workspace` target renders under the caller's own
	// identity and ignores it.
	Identity string `yaml:"identity,omitempty"`
	// VerifySignatures is the consumer-side §4.7.9 signature-verification
	// policy (never | medium-and-above | always). A standalone deployment
	// writes `never` here on first run so consumers relax the default without
	// an env var (§13.10); PODIUM_VERIFY_SIGNATURES overrides it.
	VerifySignatures string `yaml:"verify_signatures,omitempty"`
	// MinServerVersion pins the minimum MCP server / CLI binary version this
	// configuration requires (§6.7 "Versioning": a profile or harness
	// combination that needs a newer adapter behavior pins a minimum, and an
	// older binary refuses to start). A profile pin overrides this default-wide
	// pin when it is higher.
	MinServerVersion string `yaml:"min_server_version,omitempty"`
}

Defaults is the `defaults:` block.

type Duration added in v0.2.0

type Duration time.Duration

Duration is a time.Duration that unmarshals from a Go duration string such as "30s" or "5m". A bare YAML integer is rejected so a unit is always explicit, because an ambiguous "timeout: 30" reads as nanoseconds under the default yaml decoding and surprises an operator who meant seconds.

func (Duration) Duration added in v0.2.0

func (d Duration) Duration() time.Duration

Duration returns the timeout as a time.Duration.

func (*Duration) UnmarshalYAML added in v0.2.0

func (d *Duration) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML parses a duration string into d.

type EffectiveArtifact added in v0.1.3

type EffectiveArtifact struct {
	ID           string
	Type         string
	Layer        string
	Materialized bool
}

EffectiveArtifact is one row in the §7.5.5 override checklist: an artifact the caller can see, annotated with whether the target currently materializes it and the layer it resolves from.

func ResolveEffectiveView added in v0.1.3

func ResolveEffectiveView(opts Options) ([]EffectiveArtifact, error)

ResolveEffectiveView returns every artifact the caller can see, the input the §7.5.5 `podium sync override` checklist renders ("the resolved set + everything else the caller can see"). The registry source and overlay come from opts; the materialized set is read from the target's lock so each row carries its current on-disk state. Visibility is enforced by the registry source, so an artifact the caller cannot see never appears.

When opts.RegistryPath is empty there is no source to enumerate, so the view is the lock's materialized set alone (the caller can deselect but not add). spec: §7.5.5.

type ErrNoEmitter added in v0.2.0

type ErrNoEmitter struct {
	Harness string
	Err     error
}

ErrNoEmitter signals that a harness in the output's harness set has no §7.8 marketplace emitter. ValidateOutput already rejects such a harness at config validation, so Render returning it indicates a caller bypassed validation.

func (*ErrNoEmitter) Error added in v0.2.0

func (e *ErrNoEmitter) Error() string

func (*ErrNoEmitter) Unwrap added in v0.2.0

func (e *ErrNoEmitter) Unwrap() error

type GitRemote added in v0.2.0

type GitRemote struct {
	Remote string `yaml:"remote,omitempty"`
	Branch string `yaml:"branch,omitempty"`
}

GitRemote is the `git:` block of a marketplace target: the remote URL the workflow clones and pushes, and the branch it writes (§7.8).

type LockArtifact

type LockArtifact struct {
	ID               string `yaml:"id"`
	Version          string `yaml:"version,omitempty"`
	ContentHash      string `yaml:"content_hash,omitempty"`
	Layer            string `yaml:"layer,omitempty"`
	MaterializedPath string `yaml:"materialized_path,omitempty"`
	Merge            string `yaml:"merge,omitempty"`
}

LockArtifact is one entry in artifacts: per §7.5.3. Merge records the §6.7 config-merge kind for the materialized path ("json" for a JSON config-merge, "inject" for a marker-based inject, empty for a standalone file). Stale-file cleanup uses it: a standalone path is deleted when no longer written, but a config-merge path is shared with the operator, so it is reconciled (Podium's entries stripped) rather than removed.

type LockFile

type LockFile struct {
	Version int `yaml:"version"`
	// Profile is the active profile for this target. It is written without
	// omitempty so a target synced with no active profile records the explicit
	// `profile: null` the §7.5.3 schema documents rather than dropping the key.
	Profile      nullProfile    `yaml:"profile"`
	Scope        LockScope      `yaml:"scope,omitempty"`
	Harness      string         `yaml:"harness,omitempty"`
	Target       string         `yaml:"target,omitempty"`
	LastSyncedAt time.Time      `yaml:"last_synced_at,omitempty"`
	LastSyncedBy string         `yaml:"last_synced_by,omitempty"`
	Artifacts    []LockArtifact `yaml:"artifacts,omitempty"`
	Toggles      LockToggles    `yaml:"toggles,omitempty"`
}

LockFile is the per-target sync state stored at <target>/.podium/sync.lock (spec §7.5.3).

func ReadLock

func ReadLock(target string) (*LockFile, error)

ReadLock reads and parses an existing lock file at target. A missing file returns (nil, nil); other errors are returned.

type LockScope

type LockScope struct {
	Include []string `yaml:"include,omitempty"`
	Exclude []string `yaml:"exclude,omitempty"`
	Type    []string `yaml:"type,omitempty"`
}

LockScope captures the resolved scope from the active profile or CLI flags (§7.5.3).

type LockToggle

type LockToggle struct {
	ID        string    `yaml:"id"`
	Version   string    `yaml:"version,omitempty"`
	AddedAt   time.Time `yaml:"added_at,omitempty"`
	RemovedAt time.Time `yaml:"removed_at,omitempty"`
}

LockToggle is one entry in toggles.add or toggles.remove.

type LockToggles

type LockToggles struct {
	Add    []LockToggle `yaml:"add,omitempty"`
	Remove []LockToggle `yaml:"remove,omitempty"`
}

LockToggles tracks ephemeral overrides applied since the last full sync (§7.5.5).

type MergedConfig added in v0.1.3

type MergedConfig struct {
	Defaults Defaults
	Profiles map[string]Profile
	Targets  []TargetEntry
	// Collisions maps a profile name defined in more than one scope to the
	// scopes that defined it, ordered low to high precedence. §7.5.2 warns
	// when such a profile is invoked.
	Collisions map[string][]configFileScope
}

MergedConfig is the result of merging the §7.5.2 file scopes (user-global, project-shared, project-local) by per-key precedence. Defaults and Targets take the highest-precedence non-empty value; Profiles are an additive union with whole-profile overwrite on a name collision.

func LoadMergedConfig added in v0.1.3

func LoadMergedConfig(startDir, homeDir string) (*MergedConfig, string, error)

LoadMergedConfig discovers the workspace by walking up from startDir, loads the three §7.5.2 file scopes, and merges them by per-key precedence (project-local > project-shared > user-global). homeDir locates the user-global file (`<homeDir>/.podium/sync.yaml`); tests pass an explicit directory. An absent scope file contributes nothing. The returned workspace is "" when no `.podium/` is found; only the user-global scope then applies.

func (*MergedConfig) CheckServerVersion added in v0.1.3

func (cfg *MergedConfig) CheckServerVersion(binaryVersion string, profiles ...string) error

CheckServerVersion enforces the §6.7 "Versioning" pin against a merged (multi-scope) config: binaryVersion must satisfy the highest min_server_version pinned by the merged defaults or by the active profile. Pass the single resolved profile name (a `podium sync` run resolves exactly one profile). See checkServerVersion for the error codes.

type MultiTargetPlan added in v0.1.3

type MultiTargetPlan struct {
	ID       string
	Kind     string
	Registry string
	Target   string

	// Workspace-kind fields. Harness and Scope drive the project-files
	// materialization; Profile records the named profile for the run summary.
	Harness string
	Profile string
	Scope   ScopeFilter

	// Marketplace-kind fields. Identity is the §4.6 effective-view principal
	// resolved against defaults.identity, the documentary publishing identity
	// whose visibility defines what reaches the marketplace.
	Harnesses     []string
	Git           GitRemote
	CommitMessage string
	Plugins       []PluginFilter
	Identity      string

	// Workflow is the per-target prepare/publish command lists, available to
	// either kind (§7.5.2).
	Workflow Workflow
}

MultiTargetPlan is one resolved entry from a §7.5.2 `targets:` list. Each plan runs as an independent sync with its own target, scope, and lock. Kind selects the output format: "workspace" materializes the project-files layout (the Profile and Scope fields apply), and "marketplace" renders the git-repo distribution layout (§7.8) (the Harnesses, Git, CommitMessage, Plugins, and Identity fields apply). Workflow is the per-target prepare/publish command lists, carried for either kind (§7.5.2).

func PlanMultiTarget added in v0.1.3

func PlanMultiTarget(cfg *SyncConfig, in PlanInput) ([]MultiTargetPlan, error)

PlanMultiTarget resolves every entry in cfg.Targets into a runnable plan (§7.5.2 multi-target). Per entry the registry is the --config-shared registry (in.RegistryOverride or defaults.registry, resolved against in.Workspace), and the entry's kind selects the rest of the resolution.

A `kind: workspace` entry (the default for an empty kind) resolves the harness (the entry's harness then defaults.harness then "none") and the scope (the named profile merged with any inline lists, or the inline lists directly).

A `kind: marketplace` entry resolves the marketplace payload (the harness set, the git remote and branch, the commit message, the plugins, and the publishing identity inherited from defaults.identity) and skips the workspace scope resolution, so it does not become an empty-scope "none" workspace plan.

PlanMultiTarget validates each entry against its kind before resolving it: a `kind: workspace` entry rejects the marketplace fields, and a `kind: marketplace` entry rejects the workspace scope fields and the watch mode (in.Watch). A marketplace harness set naming a non-publish harness (opencode or none) is rejected. A target with no resolvable directory, an unresolved profile reference, or a kind violation is an error.

spec: §7.5.2 — "podium sync --config <path> iterates targets: and runs one sync per entry; each target writes its own lock".

type Options

type Options struct {
	RegistryPath    string
	Target          string
	AdapterID       string
	AdapterRegistry *adapter.Registry
	DryRun          bool
	// OverlayPath, when non-empty, points at a workspace overlay
	// directory whose records sit at the highest precedence of the
	// effective view (§4.6 / §6.4). Overlay artifacts override the
	// registry's contribution at the same canonical ID. The consumer merges
	// it client-side for both registry sources: the developer's overlay
	// directory is local, so a server source cannot see it.
	OverlayPath string
	// HTTPClient, when set, is used for server-source requests. Nil uses a
	// default client with a bounded timeout. Tests inject a stub here.
	HTTPClient *http.Client
	// Token is the caller credential attached as Authorization: Bearer on
	// every server-source registry API request (a §6.3.2 injected-session-token
	// or a §6.3.1 oauth-device-code access token). Empty reaches the registry
	// anonymously. It applies only to a server source; a filesystem source is
	// read locally with no request to authenticate.
	//
	// spec: §6.3.2, §14.11 — CI supplies a runtime-issued JWT via
	// PODIUM_SESSION_TOKEN_FILE so podium sync authenticates against the
	// remote registry the same way the MCP server and read CLI do.
	Token string
	// Scope narrows the effective view per §7.5.1 (--include / --exclude /
	// --type). The empty filter materializes the full effective view. The
	// resolved scope is persisted into the lock (§7.5.3).
	Scope ScopeFilter
	// Profile is the active profile name (§7.5.3). Persisted into the lock so
	// override, save-as, and profile edit can default to it.
	Profile string
	// PreserveToggles selects the §7.5.4 toggle semantics. When true (watch
	// mode and override re-materialization), Run reads the prior lock's
	// toggles, applies them on top of the scoped set (add fetched, remove
	// dropped), and carries them into the new lock. When false (a manual
	// one-shot sync), toggles are cleared, which is the operator's
	// "reset to baseline" gesture.
	PreserveToggles bool
	// LastSyncedBy is the §7.5.3 lock provenance field: one of "full"
	// (manual one-shot sync), "watch" (watcher-driven rerun), or "override"
	// (override-driven materialization). An empty value defaults to "full".
	LastSyncedBy string
	// CacheMode is the §7.4 PODIUM_CACHE_MODE the sync applies to a
	// server-source registry: "always-revalidate" (default; fetch every run),
	// "offline-first" (tolerate an unreachable server, leaving the existing
	// materialized output in place), or "offline-only" (never contact the
	// server). It is a no-op for a filesystem source, which is read locally and
	// is always reachable. An empty value behaves as always-revalidate.
	CacheMode string
}

Options are the inputs to Run. RegistryPath is the registry source: an http(s):// URL routes to a Podium server (§7.5 server-source), every other value is a filesystem-registry path (§13.11). Target is the destination directory where adapter output lands. AdapterID selects the HarnessAdapter from the registry; the default is "none" (canonical layout pass-through).

type OverrideOptions

type OverrideOptions struct {
	Target string
	// Add IDs to materialize on top of the resolved profile.
	Add []string
	// Remove IDs to drop from the resolved profile.
	Remove []string
	// Reset clears every toggle, equivalent to a manual sync.
	Reset bool
	// DryRun resolves the new state and reports without writing.
	DryRun bool
	// Clock provides timestamps; defaults to clock.Real.
	Clock clock.Clock

	// Materialization inputs (§7.5.5). When RegistryPath is set and DryRun is
	// false, Override re-materializes the target after updating the toggles:
	// --add then writes the artifact's files through the active adapter and
	// --remove deletes them, just like a full sync would. The scope and
	// profile come from the lock so the baseline matches the last sync. When
	// RegistryPath is empty, Override only records the toggles (the caller
	// materializes separately).
	RegistryPath    string
	AdapterID       string
	AdapterRegistry *adapter.Registry
	OverlayPath     string
	HTTPClient      *http.Client
}

OverrideOptions captures the §7.5.5 invocation parameters.

type OverrideResult

type OverrideResult struct {
	Lock     *LockFile
	Changed  bool
	Warnings []string
}

OverrideResult is what Override returns: the new lock state, whether anything actually changed, and any advisory warnings (e.g. a redundant --add on an already-materialized artifact, per §7.5.5).

func Override

func Override(opts OverrideOptions) (*OverrideResult, error)

Override applies the §7.5.5 toggle semantics to the lock file at opts.Target/.podium/sync.lock. Toggles persist across watcher events and survive until the next manual `podium sync` (which clears them).

type PlanInput added in v0.2.0

type PlanInput struct {
	RegistryOverride string
	Workspace        string
	Watch            bool
}

PlanInput carries the CLI-level inputs the multi-target plan needs beyond the config file: the registry override (the --registry flag), the workspace the --config file lives in (for relative registry resolution), and whether the run requested the watch mode (§7.5.4). A `kind: marketplace` target rejects the watch mode, so PlanMultiTarget needs to see it to enforce the §7.5.2 rule.

The ephemeral overrides (§7.5.5) reach the multi-target path through no field: `podium sync override` operates on a single target directory's lock file and never reads a `targets:` list, so it cannot carry a marketplace target, and the §7.5.2 rejection of an override on a marketplace entry is satisfied by the absence of a config surface that pairs the two.

type PluginFilter added in v0.2.0

type PluginFilter struct {
	Name        string   `yaml:"name"`
	Description string   `yaml:"description,omitempty"`
	Include     []string `yaml:"include,omitempty"`
	Exclude     []string `yaml:"exclude,omitempty"`
	Type        []string `yaml:"type,omitempty"`
}

PluginFilter is one entry under `plugins:`: a named bundle of selected artifacts defined by a §7.5.1 scope filter (include, exclude, type). The publishing pipeline assigns each selected artifact to its plugin by evaluating the filters in declaration order (§7.8). Description is the optional human-readable plugin description the marketplace emitter carries into the per-plugin manifest (§6.7 "Plugin descriptor"); it is empty when the operator omits it.

func (PluginFilter) ScopeFilter added in v0.2.0

func (p PluginFilter) ScopeFilter() ScopeFilter

ScopeFilter returns the §7.5.1 selection this plugin filter expresses, reusing the scope-filter machinery so plugin selection and sync selection apply identical glob semantics (§7.8).

type Profile

type Profile struct {
	Include []string `yaml:"include,omitempty"`
	Exclude []string `yaml:"exclude,omitempty"`
	Type    []string `yaml:"type,omitempty"`
	Target  string   `yaml:"target,omitempty"`
	Harness string   `yaml:"harness,omitempty"`
	// MinServerVersion pins the minimum binary version this profile requires
	// (§6.7 "Versioning"). A binary below this refuses to start when the
	// profile is active.
	MinServerVersion string `yaml:"min_server_version,omitempty"`
}

Profile is one entry under `profiles:`. Names without explicit values are normalized to empty slices on read.

type ProfileEditOptions

type ProfileEditOptions struct {
	Target        string
	Profile       string
	AddInclude    []string
	RemoveInclude []string
	AddExclude    []string
	RemoveExclude []string
	DryRun        bool
}

ProfileEditOptions captures the §7.5.7 invocation parameters.

type ProfileEditResult

type ProfileEditResult struct {
	Profile Profile
	Wrote   bool
}

ProfileEditResult reports the resulting profile.

func ProfileEdit

func ProfileEdit(opts ProfileEditOptions) (*ProfileEditResult, error)

ProfileEdit modifies an entry in sync.yaml's `profiles:` block per §7.5.7. The target directory and lock file are untouched; a subsequent `podium sync` picks up the change. The edit round-trips the file through a yaml.Node tree so comments and formatting around the untouched keys survive (§7.5.7 "preserving formatting and comments").

type Record added in v0.2.0

type Record struct {
	// ID is the canonical artifact path under the registry root.
	ID string
	// LayerID is the layer the artifact resolved from.
	LayerID string
	// Artifact is the parsed manifest. It is nil when the served frontmatter
	// did not parse, so callers that gate on the §4.3 fields guard for nil.
	Artifact *manifest.Artifact
	// ArtifactBytes is the verbatim ARTIFACT.md frontmatter (and body, for a
	// non-skill type) the adapter or emitter renders from.
	ArtifactBytes []byte
	// SkillBytes is the verbatim SKILL.md (frontmatter+body) for a skill,
	// empty otherwise.
	SkillBytes []byte
	// Resources are bundled non-manifest files keyed by relative path inside
	// the artifact directory.
	Resources map[string][]byte
	// ContentHash is the registry's authoritative §6.6 content hash for a
	// server-source record, empty for a filesystem source.
	ContentHash string
}

Record is one source-neutral artifact ready for a HarnessAdapter or a §7.8 marketplace emitter. It is the exported projection of the internal materialRecord: the marketplace render (Render) reads the same effective view as a workspace sync by calling FetchRecords, so a published marketplace reflects the publishing identity's effective view (§4.6) exactly as a workspace sync would. The fields mirror the adapter.Source inputs an emitter consumes.

func FetchRecords added in v0.2.0

func FetchRecords(opts Options) ([]Record, error)

FetchRecords resolves the caller's effective view from the registry source in opts and returns the source-neutral records, dispatching on the §7.5.2 source rule: an http(s):// RegistryPath reads the effective view over HTTP with opts.Token, every other value reads the local filesystem registry. The marketplace render and the workspace materialization both read the same view through it, so the two output kinds stay byte-equivalent for the same identity.

The §6.4 workspace overlay in opts.OverlayPath applies identically to both sources. Scope and toggles are not applied here: FetchRecords returns the full effective view, and the caller narrows it (the marketplace render intersects each plugin's scope filter; the workspace materialization applies its target scope).

spec: §7.5.2 (source dispatch), §7.8 (the marketplace render reads the same view as a workspace sync).

type RenderOptions added in v0.2.0

type RenderOptions struct {
	OutputID   string
	Registry   string
	Identity   string
	Token      string
	Workdir    string
	Harnesses  []string
	Plugins    []PluginFilter
	HTTPClient *http.Client
}

RenderOptions are the inputs to Render. Registry comes from the resolved marketplace output. Identity is the documentary record of the principal the output publishes as (§7.8 "the effective-view principal the render runs as"); the render does not read it, because the token carries the publishing identity. Workdir is the per-output checkout the render writes into (the directory the prepare phase placed a clone at). Harnesses and Plugins are the output's harness set and plugin list. Token is the publishing identity's registry credential, attached on every server-source request so the render reflects that identity's effective view (§4.6). HTTPClient is injected by tests; a nil client uses the default.

type RenderResult added in v0.2.0

type RenderResult struct {
	OutputID         string
	Changed          bool
	ChangedArtifacts []string
	Files            []string
}

RenderResult describes one render. Changed reports whether the render produced a diff against the checkout content already present in the working directory (the §7.8 $PODIUM_CHANGED signal). ChangedArtifacts lists the canonical IDs whose materialized output differs from the checkout, the body of the $PODIUM_CHANGE_SUMMARY JSON file. Files is the full set of relative paths the render wrote, sorted.

func Render added in v0.2.0

func Render(ctx context.Context, opts RenderOptions) (*RenderResult, error)

Render runs the §7.8 render phase for one marketplace output. It fetches the effective view the registry credential authenticates (§4.6), assigns each selected artifact to its plugin, renders every harness in the harness set into opts.Workdir, and reconciles the result against the prior render through the sync lock file.

The render reflects the authenticated token's effective view. opts.Identity is the documentary record of the principal the operator publishes as (§7.8 "the effective-view principal the render runs as"); the token carries the publishing identity, so Render does not read opts.Identity.

The harness set may name several Claude surfaces; they share one emitter (adapter.EmitterForHarness), so the rendered tree carries one Claude marketplace rather than a collision. A record selected by no plugin is omitted from the output.

spec: §7.8 (render pipeline), §4.6 (effective view), §7.5.1 (scope filters).

func (*RenderResult) ChangeSummaryJSON added in v0.2.0

func (r *RenderResult) ChangeSummaryJSON() []byte

ChangeSummaryJSON renders the change set as the $PODIUM_CHANGE_SUMMARY file body (§7.8): a JSON object with the output ID, whether the render changed, the count, and the changed artifact identifiers. The marketplace runner writes this to a temp file and passes its path to the workflow commands.

type ResolveInput added in v0.1.3

type ResolveInput struct {
	Registry string
	Target   string
	Harness  string
	Profile  string
	Include  []string
	Exclude  []string
	Types    []string
}

ResolveInput carries the sync CLI flag values. An empty string or slice means the flag was not given, so the next precedence level applies.

type Resolved added in v0.1.3

type Resolved struct {
	Registry string
	Target   string
	Harness  string // resolved adapter id; "none" when unset everywhere
	Profile  string // active profile name; "" when none
	Scope    ScopeFilter
	// CollisionWarning is non-empty when the invoked profile is defined in
	// more than one scope (§7.5.2). The CLI prints it to stderr.
	CollisionWarning string
}

Resolved is the merged outcome of CLI flags, `PODIUM_*` env vars, and the merged config per §7.5.2.

func Resolve added in v0.1.3

func Resolve(in ResolveInput, merged *MergedConfig, env func(string) string) (*Resolved, error)

Resolve merges CLI flags (highest precedence), `PODIUM_*` env vars, and the merged config (lowest) per §7.5.2. It selects the active profile (explicit --profile, else defaults.profile), computes the scope (CLI lists replace the profile's per field), and reports a profile-name collision. env is os.Getenv or a test stub.

spec: §7.5.2 (precedence, profile merge, collision warning).

type ResolvedOutput added in v0.2.0

type ResolvedOutput struct {
	ID            string
	Registry      string
	Identity      string
	Git           GitRemote
	Harnesses     []string
	CommitMessage string
	Plugins       []PluginFilter
	Workflow      Workflow
}

ResolvedOutput is one marketplace target with its defaults applied: the registry, the publishing identity, and the effective workflow. The git destination, harness set, plugins, and commit message come from the target entry.

type Result

type Result struct {
	Adapter   string
	Profile   string
	Target    string
	Scope     ScopeFilter
	Artifacts []ArtifactResult
	// Skipped lists the canonical IDs of artifacts excluded from this
	// run because their target_harnesses (§4.3) does not include the
	// active adapter. Recorded so callers can report what was dropped
	// rather than silently omitting it.
	Skipped []string
	// Offline is set when a §7.4 offline-first sync could not reach the
	// server-source registry and left the existing materialized output in
	// place. Callers surface it as the offline status hosts can present.
	Offline bool
	// Changed reports whether this run altered the materialized tree relative
	// to the prior lock: a path written with a different content hash, an added
	// path, or a removed (stale-cleaned) path. It is the workspace analog of the
	// marketplace render's RenderResult.Changed and feeds the $PODIUM_CHANGED
	// variable a kind: workspace target's workflow reads (§7.5.2, Decision 3), so
	// a skip_if_no_changes publish command skips a re-sync that wrote no delta. A
	// DryRun run reports the artifact set without writing, so it leaves Changed
	// false.
	Changed bool
}

Result describes what a Run actually did. Used by callers (CLI, tests) for reporting. Profile and Scope echo the resolved §7.5.1 scope and active profile so the §7.5 --json dry-run envelope can emit them.

func Run

func Run(opts Options) (*Result, error)

Run executes one sync. The registry source is dispatched per §7.5.2: an http(s):// URL reads the caller's effective view over the §7.5 HTTP API; a filesystem path reads the registry directly, applies layer composition with CollisionPolicyHighestWins (per §4.6), and writes the adapter output to Target. Both paths run the configured HarnessAdapter and the §7.5 stale-file cleanup against the same lock file.

When Options.DryRun is true, Run resolves the artifact set, returns the Result, and writes nothing.

type RunOptions added in v0.2.0

type RunOptions struct {
	Output     ResolvedOutput
	Token      string
	Workdir    string
	DryRun     bool
	Check      bool
	Stdout     io.Writer
	Stderr     io.Writer
	HTTPClient *http.Client
	Now        func() time.Time
}

RunOptions configures one RunMarketplace of a single resolved marketplace output through the prepare->render->publish pipeline. Token is the publishing identity's registry credential, forwarded to the render's effective-view fetch and exposed to commands. Workdir, when set, points the render at an existing checkout; when empty, RunMarketplace allocates a per-output working directory and the prepare phase clones into it. DryRun renders into a temporary directory and prints each prepare and publish command with its variables substituted, running no operator command and no publish phase. Check validates the config only and runs neither the render nor any command. HTTPClient and Now are injected by tests; a nil client uses the default, and a nil Now uses time.Now.

type RunResult added in v0.2.0

type RunResult struct {
	OutputID  string
	Workdir   string
	Render    *RenderResult
	Published bool
}

RunResult reports the outcome of one RunMarketplace. Workdir is the working directory the render wrote into (the allocated directory, the supplied checkout, or the dry-run temp directory). Render is the §7.8 render result; it is nil for a --check run, which does not render. Published reports whether the publish phase ran, which is false for a --dry-run or --check run.

func RunMarketplace added in v0.2.0

func RunMarketplace(ctx context.Context, opts RunOptions) (*RunResult, error)

RunMarketplace executes the prepare->render->publish pipeline for one resolved marketplace output (§7.8). The phases exist for their ordering: the prepare checkout must precede the render so the render reconciles against existing repository content, and the publish commit must follow it.

  • --check (Check) validates the config only and returns before rendering or running any command.
  • The default path allocates a working directory unless Workdir set one, runs the prepare commands, renders, then runs the publish commands.
  • --dry-run (DryRun) is a preview. It renders into a temporary directory and prints each prepare and publish command with its PODIUM_* variables substituted, running no operator command and no publish phase. Because no prepare clone populates the temporary directory, the render reconciles against an empty tree, so a dry run reports the whole rendered tree as changed.

RunMarketplace fails fast on the first command that exits non-zero, unless that command declares continue_on_error. On a failure it runs the failing phase's own on_error cleanup commands (best effort) before returning the original error: a prepare failure runs prepare_on_error, and a publish failure runs publish_on_error.

type SaveAsOptions

type SaveAsOptions struct {
	Target  string
	Profile string
	Update  bool
	DryRun  bool
}

SaveAsOptions captures the §7.5.6 invocation parameters.

type SaveAsResult

type SaveAsResult struct {
	Profile Profile
	Wrote   bool
}

SaveAsResult is what SaveAs returns.

func SaveAs

func SaveAs(opts SaveAsOptions) (*SaveAsResult, error)

SaveAs renders the current lock-file state (scope + toggles) as a profile in sync.yaml per §7.5.6. The mapping:

scope.include    → profile.include (verbatim)
scope.exclude    → profile.exclude (verbatim)
toggles.add      → profile.include (one entry per id)
toggles.remove   → profile.exclude (one entry per id)
scope.type       → profile.type (verbatim)

On success the lock file's toggles are cleared (the toggles are now part of the profile's scope).

type ScopeFilter

type ScopeFilter struct {
	Include []string
	Exclude []string
	Types   []string
}

ScopeFilter narrows a record set per §7.5.1: --include, --exclude, --type. Patterns use the same glob syntax as DOMAIN.md include: (§4.5.2): "*" matches one segment, "**" matches recursively, brace alternation works.

func (ScopeFilter) Apply

Apply runs the filter over the records and returns those that pass. When Include is non-empty, only records matching at least one include pattern survive; Exclude is then applied; Types is then applied.

func (ScopeFilter) IsEmpty

func (f ScopeFilter) IsEmpty() bool

IsEmpty reports whether the filter would match every input.

func (ScopeFilter) Select added in v0.2.0

func (f ScopeFilter) Select(records []Record) []Record

Select returns the records the scope filter selects, applying the §7.5.1 include, exclude, and type globs over the canonical artifact IDs. An empty filter selects every record (ScopeFilter.IsEmpty). It is the Record-typed form of the internal filterMaterial used by Run, so the marketplace render intersects a plugin's scope filter with the effective view through the same glob semantics `podium sync` applies to a target scope.

type SyncConfig

type SyncConfig struct {
	Defaults Defaults           `yaml:"defaults,omitempty"`
	Profiles map[string]Profile `yaml:"profiles,omitempty"`
	Targets  []TargetEntry      `yaml:"targets,omitempty"`
}

SyncConfig is the in-memory representation of `.podium/sync.yaml` (spec §7.5.2). Fields not in the schema are preserved verbatim through Read/Write so handcrafted comments and scalar formatting stay intact across edits.

func ReadConfig

func ReadConfig(workspace string) (*SyncConfig, error)

ReadConfig reads sync.yaml from the workspace's .podium/ directory. A missing file returns (nil, nil) so callers can distinguish "no config" from "invalid config" without an error type discriminator.

func ReadConfigFile added in v0.1.3

func ReadConfigFile(path string) (*SyncConfig, error)

ReadConfigFile reads a SyncConfig from an explicit path. A missing file returns (nil, nil) so callers can treat absent scopes as empty.

func (*SyncConfig) CheckServerVersion added in v0.1.3

func (cfg *SyncConfig) CheckServerVersion(binaryVersion string, profiles ...string) error

CheckServerVersion enforces the §6.7 "Versioning" pin against a raw SyncConfig: binaryVersion must satisfy the highest min_server_version pinned by defaults or by any of the named profiles. Pass every profile name for a server that can serve any profile (the MCP bridge). See checkServerVersion for the error codes.

type TargetEntry

type TargetEntry struct {
	ID      string   `yaml:"id"`
	Kind    string   `yaml:"kind,omitempty"`
	Harness string   `yaml:"harness,omitempty"`
	Target  string   `yaml:"target,omitempty"`
	Profile string   `yaml:"profile,omitempty"`
	Include []string `yaml:"include,omitempty"`
	Exclude []string `yaml:"exclude,omitempty"`
	Type    []string `yaml:"type,omitempty"`

	// Marketplace fields (valid only on a `kind: marketplace` entry, §7.5.2).
	Harnesses     []string       `yaml:"harnesses,omitempty"`
	Git           GitRemote      `yaml:"git,omitempty"`
	CommitMessage string         `yaml:"commit_message,omitempty"`
	Identity      string         `yaml:"identity,omitempty"`
	Plugins       []PluginFilter `yaml:"plugins,omitempty"`

	// Workflow is the per-target prepare/publish command lists, available to
	// either kind (§7.5.2).
	Workflow Workflow `yaml:"workflow,omitempty"`
}

TargetEntry is one entry under `targets:` (multi-target mode). Kind selects the output format (§7.5.2): "workspace" (the default, an empty value) materializes the project-files layout the harness reads, and "marketplace" renders the git-repo distribution layout (§7.8). A `kind: workspace` entry carries the harness, the target directory, and the scope fields (Profile, Include, Exclude, Type). A `kind: marketplace` entry carries the harness set (Harnesses), the git remote and branch (Git), the commit message, the plugin list, and the publishing identity. Both kinds may carry a Workflow.

type TreeWatcher added in v0.1.3

type TreeWatcher struct {
	// contains filtered or unexported fields
}

TreeWatcher wraps an fsnotify.Watcher to observe one or more directory trees recursively. fsnotify watches individual directories, so the watcher adds every existing subdirectory up front and adds newly created subdirectories as they appear, keeping the whole registry and overlay trees observed. It is exported so the MCP workspace-overlay watcher (spec §6.4, cmd/podium-mcp/overlay_watch.go) reuses the same recursive fsnotify watch as `podium sync --watch`. spec: §13.11.4, §6.4.

func NewTreeWatcher added in v0.1.3

func NewTreeWatcher(paths ...string) (*TreeWatcher, error)

NewTreeWatcher creates a recursive watcher over the given paths. An empty path is skipped. A path that does not exist contributes no watches; its parent directory is watched instead (when present) so the path's creation is detected. Returns an error only when fsnotify itself cannot initialize, which is the signal for the caller to fall back to polling.

func (*TreeWatcher) AddTree added in v0.1.3

func (t *TreeWatcher) AddTree(root string)

AddTree adds root and every subdirectory under it to the watcher. A missing root is tolerated: its parent directory is watched (when it exists) so the root's later creation surfaces as an event.

func (*TreeWatcher) Close added in v0.1.3

func (t *TreeWatcher) Close() error

Close releases the underlying fsnotify watcher.

func (*TreeWatcher) Errors added in v0.1.3

func (t *TreeWatcher) Errors() <-chan error

Errors returns the underlying fsnotify error channel.

func (*TreeWatcher) Events added in v0.1.3

func (t *TreeWatcher) Events() <-chan fsnotify.Event

Events returns the underlying fsnotify event channel.

type WatchEvent

type WatchEvent struct {
	Result *Result
	Err    error
}

WatchEvent reports one Run invocation triggered by Watch. Callers receive an event for the initial sync and for every subsequent rerun. Err is non-nil when the run itself failed; the watcher continues running.

type WatchOptions

type WatchOptions struct {
	// Sync supplies the underlying Run options. RegistryPath is
	// always watched; Target receives the materialized output.
	Sync Options
	// OverlayPath, when non-empty, is watched alongside the
	// registry path so workspace overlay edits trigger a rerun.
	OverlayPath string
	// Period is the poll interval. Defaults to 500ms.
	Period time.Duration
	// Debounce delays a rerun until at least this duration has
	// passed since the last detected change. Defaults to 200ms.
	Debounce time.Duration
}

WatchOptions configures the long-running --watch mode (§7.5).

Watch reruns Run whenever the registry path or overlay path changes. Detection uses fsnotify (§13.11.4); polling is the fallback for platforms or filesystems where fsnotify cannot initialize, where the poller computes a content-fingerprint over each watched tree and reruns the sync when the fingerprint moves. Debounce coalesces bursts of edits into a single sync.

type Workflow added in v0.2.0

type Workflow struct {
	Prepare        []Command `yaml:"prepare,omitempty"`
	Publish        []Command `yaml:"publish,omitempty"`
	PrepareOnError []Command `yaml:"prepare_on_error,omitempty"`
	PublishOnError []Command `yaml:"publish_on_error,omitempty"`
}

Workflow groups the `prepare` and `publish` command lists `podium sync` runs around the render phase of a target (§7.5.2, §7.8). prepare places a checkout of the destination repository at the working directory, and publish takes the rendered tree to the remote. PrepareOnError and PublishOnError are optional per-phase cleanup lists: a failure in the prepare phase runs PrepareOnError, and a failure in the publish phase runs PublishOnError, before the failure propagates. The cleanup is scoped to the phase that failed, so a prepare failure does not run cleanup authored for a publish-phase checkout.

func (Workflow) IsZero added in v0.2.0

func (w Workflow) IsZero() bool

IsZero reports whether the workflow declares no commands.

type WorkflowRunner added in v0.2.0

type WorkflowRunner struct {
	Label  string
	Stdout io.Writer
	Stderr io.Writer
}

WorkflowRunner executes a target's workflow command lists (§7.5.2). It is the shared command machinery both the marketplace pipeline (RunMarketplace) and the workspace dispatch (a kind: workspace target carrying a workflow) run through, so the prepare/publish phases, the per-command flags, and the on_error cleanup behave identically for either kind. Label names the target in error messages and skip logs so the operator can tell which target a failure belongs to.

func (WorkflowRunner) Phase added in v0.2.0

func (r WorkflowRunner) Phase(ctx context.Context, phase string, cmds []Command, vars map[string]string, onError []Command) error

Phase runs the commands of one workflow phase in order (§7.5.2, §7.8), failing fast on the first non-zero exit unless the command declares continue_on_error. A skip_if_no_changes command is skipped when vars["PODIUM_CHANGED"] is "false". On a failure it runs that phase's own on_error cleanup commands (best effort, errors logged not returned) before returning the original failure, so a half-applied checkout can be reset by cleanup scoped to the failing phase.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL