sync

package
v1.5.0 Latest Latest
Warning

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

Go to latest
Published: May 13, 2026 License: MIT Imports: 32 Imported by: 0

Documentation

Index

Constants

View Source
const DefaultMaxVersions = 10

DefaultMaxVersions is the default number of version snapshots to retain.

Variables

This section is empty.

Functions

func CommandHash

func CommandHash(install, update string, installs, updates map[string]string) string

CommandHash returns a deterministic hash of all install/update commands, including per-tool overrides. Used for TOFU: if the hash changes, the user must re-approve. Adding per-tool commands to a previously global-only entry will change the hash and trigger re-approval.

func ComputeFileHash

func ComputeFileHash(content []byte) string

ComputeFileHash returns SHA-256 hash (first 8 hex chars) of file content. Normalizes CRLF → LF for cross-platform determinism (must match discovery.skillFileHash).

func EnforceRetention

func EnforceRetention(skillDir string, maxVersions int) error

EnforceRetention deletes oldest snapshots if count exceeds maxVersions. If maxVersions is 0, all snapshots are kept (unlimited).

func HasConflictMarkers

func HasConflictMarkers(content []byte) bool

func HashInstallableFiles

func HashInstallableFiles(files []tools.SkillFile) (string, error)

func IsLocallyModified

func IsLocallyModified(skillDir string, installedHash string) bool

IsLocallyModified checks if SKILL.md has been modified since last sync. When .scribe-base.md is readable, it is the source of truth and is compared directly with SKILL.md. When .scribe-base.md is missing, installedHash is the legacy fallback for installs that do not have a sidecar yet. Other sidecar read errors are treated conservatively as locally modified.

func RunPackageCommand

func RunPackageCommand(ctx context.Context, exec CommandExecutor, pkgDir, cmd string, timeout time.Duration) (string, string, error)

RunPackageCommand executes cmd inside pkgDir using the provided executor. Stdout/stderr are captured. Empty cmd is a no-op (returns nil). A non-nil err indicates the command failed or the context was cancelled; callers are expected to roll back on install errors.

func ShortSHA

func ShortSHA(sha string) string

ShortSHA truncates a commit SHA to 7 characters for display.

func SnapshotVersion

func SnapshotVersion(skillDir string, revision int) error

SnapshotVersion copies current SKILL.md to versions/rev-{N}.md before content changes. N is the current revision number from state. If the SKILL.md does not exist, this is a no-op (first install).

Types

type BudgetWarningMsg

type BudgetWarningMsg struct {
	Agent   string
	Message string
}

BudgetWarningMsg is emitted when a post-change projected skill set enters an agent's warning band but remains below the hard refusal limit.

type CommandExecutor

type CommandExecutor interface {
	Execute(ctx context.Context, command string, timeout time.Duration) (stdout, stderr string, err error)
}

CommandExecutor runs shell commands and captures output.

type GitHubFetcher

type GitHubFetcher interface {
	FetchFile(ctx context.Context, owner, repo, path, ref string) ([]byte, error)
	FetchDirectory(ctx context.Context, owner, repo, dirPath, ref string) ([]SkillFile, error)
	LatestCommitSHA(ctx context.Context, owner, repo, branch string) (string, error)
	GetTree(ctx context.Context, owner, repo, ref string) ([]provider.TreeEntry, error)
}

GitHubFetcher abstracts GitHub API operations needed by the sync engine.

func WrapGitHubClient

func WrapGitHubClient(c *gh.Client) GitHubFetcher

WrapGitHubClient returns a GitHubFetcher backed by a real gh.Client. Delegates to provider.WrapGitHubClient — provider.GitHubClient is a superset of GitHubFetcher.

type Kind

type Kind string

Kind classifies a fetched repo payload.

KindSkill: single-authorship skill; dir-symlinked into tool skill dirs. KindPackage: multi-skill bundle or self-installing toolkit; stored under ~/.scribe/packages/<name>/ and never projected into agent skill dirs.

const (
	KindSkill   Kind = "skill"
	KindPackage Kind = "package"
)

func DetectKind

func DetectKind(files []tools.SkillFile) Kind

DetectKind classifies a fetched file tree as either a skill or a package.

Rules (first match wins):

  1. Any SKILL.md at a non-root path (e.g. "browse/SKILL.md") → package.
  2. No SKILL.md at the root AND the tree has a recognised install script (setup, install.sh, bootstrap, Makefile, package.json) → package.
  3. Otherwise → skill.

This mirrors the spec at docs/superpowers/specs/2026-04-17-packages-store-design.md. The detection runs against in-memory SkillFile slices before the tree is written to disk, so nothing is persisted until routing is decided.

func DetectKindFromDir

func DetectKindFromDir(dir string) (Kind, error)

DetectKindFromDir classifies an existing on-disk directory. Used by the migration pass: first sync after upgrade walks ~/.scribe/skills/<name>/ for each installed skill and reclassifies any that look like packages.

type LegacyFormatMsg

type LegacyFormatMsg struct{ Repo string }

LegacyFormatMsg is sent when a registry still uses scribe.toml (TOML format).

type LockMismatchError

type LockMismatchError struct {
	Registry string        `json:"registry"`
	Refused  []LockRefusal `json:"refused"`
}

func (*LockMismatchError) Error

func (e *LockMismatchError) Error() string

type LockRefusal

type LockRefusal struct {
	Name         string `json:"name"`
	ExpectedHash string `json:"expected_hash"`
	ActualHash   string `json:"actual_hash"`
	Reason       string `json:"reason"`
}

type MergeConflictMsg

type MergeConflictMsg struct {
	Name string
}

MergeConflictMsg is sent when a 3-way merge produces conflict markers.

type MergeResult

type MergeResult int

MergeResult describes the outcome of a 3-way merge.

const (
	MergeClean    MergeResult = iota // clean merge, auto-applied
	MergeConflict                    // has conflict markers
	MergeError                       // git merge-file failed
)

func ThreeWayMerge

func ThreeWayMerge(skillDir string, upstreamContent []byte) (MergeResult, error)

ThreeWayMerge performs a 3-way merge using git merge-file. base = .scribe-base.md (last synced pristine) ours = SKILL.md (locally modified) theirs = new upstream content

On clean merge: updates SKILL.md in place, advances .scribe-base.md to the new upstream content, and removes any stale .scribe-theirs.md sidecar. On conflict: writes conflict markers to SKILL.md, keeps .scribe-base.md unchanged (still the pre-merge base), and persists .scribe-theirs.md so `scribe resolve --theirs` can read the upstream version.

type ModifiedStrategy

type ModifiedStrategy int

ModifiedStrategy controls how sync treats outdated skills with local edits.

const (
	ModifiedStrategyMerge ModifiedStrategy = iota
	ModifiedStrategyPreferTheirs
)

type NameConflict

type NameConflict struct {
	Name string
	Tool string
	Path string
}

type NameConflictAction

type NameConflictAction string
const (
	NameConflictActionUnresolved NameConflictAction = "unresolved"
	NameConflictActionAdopt      NameConflictAction = "adopt"
	NameConflictActionAlias      NameConflictAction = "alias"
	NameConflictActionSkip       NameConflictAction = "skip"
)

type NameConflictError

type NameConflictError struct {
	Conflict   NameConflict
	Resolution NameConflictResolution
	Err        error
}

func (*NameConflictError) Error

func (e *NameConflictError) Error() string

func (*NameConflictError) Unwrap

func (e *NameConflictError) Unwrap() error

type NameConflictResolution

type NameConflictResolution struct {
	Action NameConflictAction `json:"action"`
	Alias  string             `json:"alias,omitempty"`
}

type NameConflictResolvedMsg

type NameConflictResolvedMsg struct {
	Conflict   NameConflict
	Resolution NameConflictResolution
}

type NoopFetcher

type NoopFetcher struct{}

NoopFetcher is a GitHubFetcher that returns errors for all operations. Used when Provider handles all fetching.

func (*NoopFetcher) FetchDirectory

func (n *NoopFetcher) FetchDirectory(_ context.Context, _, _, _, _ string) ([]SkillFile, error)

func (*NoopFetcher) FetchFile

func (n *NoopFetcher) FetchFile(_ context.Context, _, _, _, _ string) ([]byte, error)

func (*NoopFetcher) GetTree

func (n *NoopFetcher) GetTree(_ context.Context, _, _, _ string) ([]provider.TreeEntry, error)

func (*NoopFetcher) LatestCommitSHA

func (n *NoopFetcher) LatestCommitSHA(_ context.Context, _, _, _ string) (string, error)

type PackageApprovedMsg

type PackageApprovedMsg struct{ Name string }

PackageApprovedMsg is sent when a user approves a package install.

type PackageDeniedMsg

type PackageDeniedMsg struct{ Name string }

PackageDeniedMsg is sent when a user denies a package install.

type PackageDetectedMsg

type PackageDetectedMsg struct {
	Name   string
	Dir    string
	Source string // where the install command came from (scribe.yaml, ./setup, …)
}

PackageDetectedMsg is emitted when a fetched payload was classified as a tree-package (nested SKILL.md or install script). Lets the UI show a "stored as package" hint before the install command runs.

type PackageErrorMsg

type PackageErrorMsg struct {
	Name   string
	Err    error
	Stderr string
}

PackageErrorMsg is sent when a package install or update fails.

type PackageHashMismatchMsg

type PackageHashMismatchMsg struct {
	Name       string
	OldCommand string
	NewCommand string
	Source     string
}

PackageHashMismatchMsg is sent when a previously approved command has changed.

type PackageInstallPlan

type PackageInstallPlan struct {
	// Command is the shell command to run after writing the package dir.
	// Executed from within the package directory.
	Command string
	// Uninstall, if set, is the command to run when `scribe remove <name>`
	// is invoked for this package. Best-effort.
	Uninstall string
	// Source is a short description of where the command came from
	// ("scribe.yaml", "./setup", "install.sh", "package.json", "Makefile").
	Source string
}

PackageInstallPlan describes how a staged package should self-install. Resolution order (first match wins):

  1. scribe.yaml → install.command / install.uninstall
  2. executable setup at repo root
  3. install.sh at repo root
  4. package.json → run `bun install` when bun is available, else `npm install`
  5. Makefile target install (run via `make install`)
  6. No install command → empty Command (package is tracked but no-op)

func ResolvePackageInstall

func ResolvePackageInstall(pkgDir string) (PackageInstallPlan, error)

ResolvePackageInstall walks the given package directory and returns a plan for how to execute its self-install. See PackageInstallPlan for the resolution order. An absent plan (empty Command) is not an error — the package is tracked but triggers no shell execution.

type PackageInstallPromptMsg

type PackageInstallPromptMsg struct {
	Name    string
	Command string
	Source  string
}

PackageInstallPromptMsg is sent when a package requires user approval.

type PackageInstalledMsg

type PackageInstalledMsg struct{ Name string }

PackageInstalledMsg is sent when a package install completes successfully.

type PackageInstallingMsg

type PackageInstallingMsg struct{ Name string }

PackageInstallingMsg is sent when a package install command begins.

type PackageOutputMsg

type PackageOutputMsg struct {
	Name   string
	Stdout string
	Stderr string
}

PackageOutputMsg streams a package install/uninstall command's combined output to the UI. Emitted once per command, after it finishes.

type PackageReclassifiedMsg

type PackageReclassifiedMsg struct {
	Name        string
	OldPath     string
	NewPath     string
	InstallHint string
}

PackageReclassifiedMsg is emitted by the migration pass when a legacy skills/ install is moved into packages/ because its tree shape identifies it as a package. InstallHint carries a note about whether setup should be re-run (we don't auto-run during migration).

type PackageSkippedMsg

type PackageSkippedMsg struct {
	Name   string
	Reason string
}

PackageSkippedMsg is sent when a package is skipped (e.g. already approved).

type PackageUpdateMsg

type PackageUpdateMsg struct{ Name string }

PackageUpdateMsg is sent when a package update command begins.

type PackageUpdatedMsg

type PackageUpdatedMsg struct{ Name string }

PackageUpdatedMsg is sent when a package update completes successfully.

type ProjectLockError

type ProjectLockError struct {
	Refused []LockRefusal `json:"refused"`
}

func (*ProjectLockError) Error

func (e *ProjectLockError) Error() string

type ReconcileCompleteMsg

type ReconcileCompleteMsg struct {
	Summary reconcile.Summary
}

type ReconcileConflictMsg

type ReconcileConflictMsg struct {
	Name     string
	Conflict state.ProjectionConflict
}

type ShellExecutor

type ShellExecutor struct{}

ShellExecutor runs commands via a platform shell with process group management where supported.

func (*ShellExecutor) Execute

func (e *ShellExecutor) Execute(ctx context.Context, command string, timeout time.Duration) (string, string, error)

type SkillAdoptionNeededMsg

type SkillAdoptionNeededMsg struct {
	Name string
	Tool string
	Path string
}

SkillAdoptionNeededMsg is sent when install refused to overwrite a real (non-Scribe) directory at a tool projection path. Run `scribe adopt <name>` to import the existing content, then re-sync.

type SkillDownloadingMsg

type SkillDownloadingMsg struct{ Name string }

SkillDownloadingMsg is sent when a skill download begins.

type SkillErrorMsg

type SkillErrorMsg struct {
	Name string
	Err  error
}

SkillErrorMsg is sent when a skill fails to install. Sync continues.

type SkillFile

type SkillFile = tools.SkillFile

SkillFile is a single file within a downloaded skill directory.

type SkillInstalledMsg

type SkillInstalledMsg struct {
	Name     string
	Updated  bool // true = update, false = fresh install
	Merged   bool // true if 3-way merge was used
	Revision int  // post-install revision number (1 for fresh install)
}

SkillInstalledMsg is sent when a skill is successfully installed or updated.

type SkillResolvedMsg

type SkillResolvedMsg struct{ SkillStatus }

SkillResolvedMsg is sent once per skill after the diff is computed, before any downloads start. Powers the initial list render.

type SkillSkippedByDenyListMsg

type SkillSkippedByDenyListMsg struct {
	Name     string
	Registry string
}

SkillSkippedByDenyListMsg is sent when a user removed a registry skill and sync is preserving that removal intent.

type SkillSkippedMsg

type SkillSkippedMsg struct{ Name string }

SkillSkippedMsg is sent when a skill is already current — no action needed.

type SkillStatus

type SkillStatus struct {
	Name       string
	Status     Status
	Installed  *state.InstalledSkill // nil if not installed
	Entry      *manifest.Entry       // catalog entry, nil for StatusExtra
	LoadoutRef string                // the ref from the manifest (e.g. "v1.0.0", "main")
	Maintainer string
	IsPackage  bool
	LatestSHA  string // resolved SHA for branch-pinned skills; empty if unavailable
	BlobSHAs   map[string]string
	LockEntry  *lockfile.Entry

	SourceKey   string
	Source      *source.SourceSpec
	ResolvedRev string
}

SkillStatus is the result of comparing one skill against the loadout.

func (SkillStatus) DisplayAgents

func (sk SkillStatus) DisplayAgents() string

DisplayAgents returns the comma-separated list of installed targets.

func (SkillStatus) DisplayAuthor

func (sk SkillStatus) DisplayAuthor() string

DisplayAuthor returns the author or "—" if unknown.

func (SkillStatus) DisplayVersion

func (sk SkillStatus) DisplayVersion() string

DisplayVersion returns the best human-readable version for this skill. Prefers semver tags as-is, else a short SHA when the ref is a branch/HEAD, else the installed revision counter.

type Status

type Status int

Status describes how a skill compares against the team loadout.

const (
	StatusMissing    Status = iota // in loadout, not installed locally
	StatusCurrent                  // installed, matches loadout exactly
	StatusOutdated                 // installed, but loadout specifies a different version
	StatusExtra                    // installed locally, not in the team loadout
	StatusModified                 // installed, locally modified, upstream unchanged
	StatusConflicted               // merge produced conflicts, needs resolution
)

func (Status) Display

func (s Status) Display() StatusDisplay

Display returns the icon and label for rendering this status.

func (Status) String

func (s Status) String() string

type StatusDisplay

type StatusDisplay struct {
	Icon  string
	Label string
}

StatusDisplay holds the icon and label for a status value.

type SyncCompleteMsg

type SyncCompleteMsg struct {
	Installed int
	Updated   int
	Skipped   int
	Failed    int
}

SyncCompleteMsg is sent when all skills have been processed.

type Syncer

type Syncer struct {
	Client   GitHubFetcher
	Provider provider.Provider // optional — if set, used for discovery and fetch
	Tools    []tools.Tool
	Emit     func(any) // receives events defined in events.go
	Executor CommandExecutor
	// ProjectRoot scopes tool projections to a project-local agent directory.
	// Empty preserves legacy global projections.
	ProjectRoot string

	// ModifiedStrategy controls what to do when an outdated skill also has
	// unsynced local edits on disk.
	ModifiedStrategy ModifiedStrategy

	// SkillFilter, if non-nil, restricts the sync to only the named skills.
	// Skills not in the list are skipped entirely (not even resolved/emitted).
	// Used by `scribe install` to install specific skills.
	SkillFilter []string

	// KitFilter, if non-nil, restricts symlink emission to skills resolved by
	// the project's kit set. Empty/nil means no kit filtering (legacy behavior).
	// Computed from the project file by StepResolveKitFilter so `scribe sync`
	// matches `scribe show` output.
	KitFilter        []string
	KitFilterEnabled bool

	// SkipMissing prevents installing skills that are not yet locally installed.
	// When true, StatusMissing skills are silently skipped — only updates and
	// removals are processed. Set by `scribe sync` to implement the opt-in
	// model: new skills from a registry are not auto-installed; the user must
	// explicitly run `scribe add <skill>` or `scribe install` to install them.
	SkipMissing bool

	// TrustAll skips approval prompts for packages (--trust-all flag).
	TrustAll bool

	// ForceBudget allows projection even when an agent description-byte budget
	// would otherwise be exceeded.
	ForceBudget bool

	// ApprovalFunc is called when a package needs interactive approval.
	// Returns true if approved, false if denied.
	// If nil and TrustAll is false, packages needing approval are skipped.
	ApprovalFunc func(name, command, source string) bool

	// AliasName installs an incoming skill under this alternate name when a
	// real directory already exists at the original tool projection path.
	AliasName string

	// SkillAliases installs named incoming skills under fixed local names.
	// Used by registry kits to avoid same-name collisions across sources.
	SkillAliases map[string]string

	// PinnedSkillSources overrides catalog source refs for named skills during
	// kit dependency installs.
	PinnedSkillSources map[string]string

	// OnRegistryFetched runs after a registry manifest has been fetched and
	// applied successfully. Callers use this for local metadata caches.
	OnRegistryFetched func(repo string, m *manifest.Manifest) error

	// NameConflictResolver is called when a real directory already exists at
	// the incoming skill name. Nil means non-interactive conflict.
	NameConflictResolver func(NameConflict) (NameConflictResolution, error)
}

Syncer wires manifest, github, tools, and state together. It emits events via the Emit callback — the caller decides whether to forward them to a Bubbletea program or log them to stdout.

func (*Syncer) Diff

func (s *Syncer) Diff(ctx context.Context, teamRepo string, st *state.State) ([]SkillStatus, *manifest.Manifest, error)

Diff fetches the team loadout and computes status for every skill without making any changes. Used by `scribe list`. Returns the parsed manifest alongside statuses so callers can reuse it.

func (*Syncer) DiffSource

func (s *Syncer) DiffSource(ctx context.Context, registryKey string, spec source.SourceSpec, st *state.State) ([]SkillStatus, *manifest.Manifest, error)

DiffSource fetches a SourceSpec-backed registry and computes status for each skill. registryKey is the legacy/display identity used in state and JSON during phase 2.

func (*Syncer) FetchLockfile

func (s *Syncer) FetchLockfile(ctx context.Context, teamRepo string) (*lockfile.Lockfile, error)

func (*Syncer) FetchManifest

func (s *Syncer) FetchManifest(ctx context.Context, owner, repo string) (*manifest.Manifest, error)

FetchManifest tries Provider.Discover first (if set), then falls back to direct file fetch with scribe.yaml → scribe.toml fallback.

func (*Syncer) FetchManifestSource

func (s *Syncer) FetchManifestSource(ctx context.Context, spec source.SourceSpec) (*manifest.Manifest, error)

FetchManifestSource fetches a manifest through SourceSpec-aware providers. It falls back to legacy GitHub file fetch only for unscoped GitHub sources.

func (*Syncer) ReclassifyLegacyPackages

func (s *Syncer) ReclassifyLegacyPackages(st *state.State) error

ReclassifyLegacyPackages scans state entries classified as skills, detects any whose on-disk shape is actually a package (nested SKILL.md / install script), and moves them to ~/.scribe/packages/<name>/. Tool projections are removed and state is updated. Each move emits a PackageReclassifiedMsg so the UI can surface a hint.

Safe to call repeatedly — the caller guards with state.HasMigration and marks it done after the first successful pass.

func (*Syncer) Run

func (s *Syncer) Run(ctx context.Context, teamRepo string, st *state.State) error

Run executes the full sync: diff, then install/update as needed. Emits events throughout. Updates state incrementally — a failed skill does not prevent successful skills from being recorded.

func (*Syncer) RunProject

func (s *Syncer) RunProject(ctx context.Context, st *state.State, lf *lockfile.ProjectLockfile) error

func (*Syncer) RunSource

func (s *Syncer) RunSource(ctx context.Context, teamRepo string, spec source.SourceSpec, st *state.State) error

RunSource executes a sync against a SourceSpec-backed registry.

func (*Syncer) RunWithDiff

func (s *Syncer) RunWithDiff(ctx context.Context, teamRepo string, statuses []SkillStatus, st *state.State) error

RunWithDiff applies a pre-computed diff (statuses) directly. Used by tests and callers that already have statuses from Diff().

func (*Syncer) RunWithDiffSource

func (s *Syncer) RunWithDiffSource(ctx context.Context, teamRepo string, spec source.SourceSpec, statuses []SkillStatus, st *state.State) error

RunWithDiffSource applies a pre-computed diff using SourceSpec-aware fetch.

type TreeEntry

type TreeEntry = provider.TreeEntry

Re-export so external callers can build their own fetchers without importing the provider package.

type VersionInfo

type VersionInfo struct {
	Revision int
	Path     string
	ModTime  time.Time
}

VersionInfo describes a single version snapshot.

func ListVersions

func ListVersions(skillDir string) ([]VersionInfo, error)

ListVersions returns available version snapshots for a skill, sorted by revision (ascending).

Jump to

Keyboard shortcuts

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