linker

package
v1.32.0 Latest Latest
Warning

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

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package linker owns the decision half of registering a directory as a lerd site. Every caller that links a project — the CLI, the parked-directory watcher, the web UI and the MCP server — resolves the same plan here and differs only in the capabilities it grants through a Policy.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func FilterConflictingDomains

func FilterConflictingDomains(desired []string, ownPath string, allSites []config.Site) (kept, removed []string)

FilterConflictingDomains splits desired into the domains ownPath may claim and those a different site already holds. The check is strict: a domain is a conflict regardless of TLS scheme, because DNS and browser caches don't disambiguate by scheme reliably. Order is preserved so a surviving preferred domain stays primary. Re-linking the same path is not a conflict.

func FreeSiteName

func FreeSiteName(desired, path string) string

FreeSiteName returns the first available site name for a path. An unused name is returned as-is, as is one already held by the same path (a re-link). A name held by a different path gets "-2", "-3", … until one is free.

func HostProxyGate

func HostProxyGate(command string, disabled, skipConfirm, approved, canPrompt bool) (proceed, ask bool, reason string)

HostProxyGate is the decision for whether lerd may supervise a dev-server command on the host. It is pure so the policy is testable without a terminal or a config file. proceed reports that the command may run without asking; ask reports that a confirmation is still owed; reason explains a refusal. A site only ever runs a command the user has consented to.

func IsReservedDomain

func IsReservedDomain(domain string) bool

IsReservedDomain reports whether a domain is reserved for lerd's own use.

func OwningWorktree

func OwningWorktree(dir string) (*config.Site, string, bool)

OwningWorktree returns the site dir is a git worktree of, so a checkout under a registered project is not registered again as a site of its own.

func ResolveDomains

func ResolveDomains(desired []string, baseName, ownPath, tld string) (kept, removed []string)

ResolveDomains filters the desired domain list against the live registry and returns the list to register. When every desired domain is conflicted it falls back to a freshly generated `<baseName>.<tld>`, suffixed until it is free in both the name and domain axes. The .lerd.yaml on disk is never touched; the discrepancy lives only in the registration, and the dropped domains come back in removed so the caller can report them.

func ResolveFramework

func ResolveFramework(dir string, allowStoreFallback bool) (string, bool)

ResolveFramework names the framework for dir. The store fallback asks the user which definition to install, so it is only reachable when the caller can put a question on screen.

Types

type Deps

type Deps struct {
	// EnsureFPMQuadlet builds a PHP version's image and unit.
	EnsureFPMQuadlet func(version string) error
	// ReconcileRuntimeQuadlets clears a per-site FrankenPHP or custom-FPM unit
	// the site no longer uses.
	ReconcileRuntimeQuadlets func(site config.Site)
	// StartHostProxyWorker supervises a host-proxy site's dev command.
	StartHostProxyWorker func(site config.Site, proxy *config.ProxyConfig)
	// SyncIDEDataSource points a JetBrains project at the site's database and
	// reports whether it wrote anything.
	SyncIDEDataSource func(siteRoot string) bool
}

Deps are the side effects a link needs that live above this package. A nil field means the caller cannot do that thing, and the step is skipped.

type Mode

type Mode string

Mode is how a linked site is served.

const (
	ModeFPM             Mode = "fpm"
	ModeFrankenPHP      Mode = "frankenphp"
	ModeCustomFPM       Mode = "fpm-custom"
	ModeCustomContainer Mode = "container"
	ModeHostProxy       Mode = "host-proxy"
)

type NopReporter

type NopReporter struct{}

NopReporter discards everything, for callers that report on their own.

func (NopReporter) Line

func (NopReporter) Line(string)

func (NopReporter) Step

func (NopReporter) Step(string) Step

func (NopReporter) Val

func (NopReporter) Val(s string) string

func (NopReporter) Warn

func (NopReporter) Warn(string, ...any)

type Plan

type Plan struct {
	// Dir is the directory being linked, as given.
	Dir string
	// Site is the registration that will be written.
	Site config.Site
	// Mode is how Site will be served.
	Mode Mode
	// Skip is set when the directory will not be registered at all, and
	// SkipDetail explains it in words the caller can print.
	Skip       Skip
	SkipDetail string
	// WorktreeParent and WorktreeBranch are set alongside SkipWorktree: the
	// site that owns this checkout, and the branch it holds.
	WorktreeParent *config.Site
	WorktreeBranch string
	// Project is the parsed .lerd.yaml, or nil when the project has none.
	Project *config.ProjectConfig
	// DroppedDomains lists domains another site already owns, which were
	// filtered out of Site.Domains.
	DroppedDomains []string
	// PHPSuggestion names a PHP version that suits the framework better than
	// the one Site carries. Empty when the chosen version is already the best
	// installed one, or when the policy forbids building images.
	PHPSuggestion string
	// PHPMin and PHPMax are the framework's supported range, for the message
	// that accompanies a clamped version. Both empty when unconstrained.
	PHPMin, PHPMax string
	// FrameworkLabel names the framework in user-facing messages, and is
	// "unknown (public: <dir>)" when detection found none.
	FrameworkLabel string
	// FrankenPHPDeclined records that the project asked for FrankenPHP but no
	// image exists for its PHP version, so the site falls back to FPM.
	FrankenPHPDeclined bool
	// ProxyCommand is the host dev-server command awaiting consent, empty when
	// the site runs none.
	ProxyCommand string
}

Plan is everything a link decided before it changed anything. Resolve produces it from the directory, the global config and the policy; Apply carries it out.

func Resolve

func Resolve(dir string, cfg *config.GlobalConfig, p Policy) (*Plan, error)

Resolve decides everything about linking dir as a site without changing anything. It reads the directory, the project's .lerd.yaml and the site registry, and returns the plan Apply carries out. A plan whose Skip is set registers nothing.

func (*Plan) Registered

func (p *Plan) Registered() bool

Registered reports whether the plan will actually register a site.

type Policy

type Policy struct {
	// Name overrides the site name and primary domain. Empty derives both from
	// the directory name.
	Name string
	// AssumeYes records consent given outside a prompt, such as `lerd link
	// --yes` or the click that started a link from the web UI.
	AssumeYes bool
	// Prompt, when non-nil, may ask the user to resolve a decision. Nil means
	// no question can be asked, whether or not a terminal is attached.
	Prompt Prompter
	// ProjectWrites allows writing into the project directory: the .php-version
	// and .node-version pins, and the .lerd.yaml domain and framework writeback.
	ProjectWrites bool
	// Services allows installing and starting the services the project declares
	// and the ones its framework requires.
	Services bool
	// Certs allows the site to be registered as secured, which issues a
	// certificate for it.
	Certs bool
	// RepoCommands allows running commands the repository authored: a
	// host-proxy dev server and inline service containers. Consent for these
	// still goes through the usual gate; this only says the caller is a context
	// where running them is on the table at all.
	RepoCommands bool
	// ImageBuild allows building or pulling a PHP image the host does not have.
	ImageBuild bool
	// SkipRegistered stops the link when the directory is already a registered
	// site, rather than re-linking it.
	SkipRegistered bool
	// DeferPublish leaves the reloads that publish a link — systemd, the quadlet
	// rewrite, the container hosts file and nginx — to the caller, so a batch of
	// links does that work once at the end instead of once per project.
	DeferPublish bool
}

Policy is the capability set a caller grants a link. The zero value is the most restrictive one: it registers a site and serves it, and does nothing else. Each field widens that.

func CLIPolicy

func CLIPolicy(name string, assumeYes bool, prompt Prompter) Policy

CLIPolicy is the policy for a user-invoked `lerd link`: everything is permitted, and prompt decides whether questions can be asked.

func WatcherPolicy

func WatcherPolicy() Policy

WatcherPolicy is the policy for `lerd park` and the parked-directory watcher. It runs unattended against every subdirectory of a parked tree, so it reads the project's committed configuration but never asks a question, never writes into the project, and never runs anything the repository authored.

type Prompter

type Prompter interface {
	// Confirm asks a yes/no question and reports the answer.
	Confirm(question string, defaultYes bool) bool
	// Choose asks the user to pick one of options and returns its index.
	Choose(title string, options []string) (int, error)
}

Prompter resolves a question a link cannot answer on its own. Callers with a terminal supply one; every other caller passes nil, and the link takes the conservative branch instead of blocking on input that will never arrive.

type Reporter

type Reporter interface {
	// Step announces work that is starting and returns the handle that closes it.
	Step(label string) Step
	// Line reports something worth showing that is not a step.
	Line(msg string)
	// Warn reports a problem that did not stop the link.
	Warn(format string, a ...any)
	// Val styles a value inside a message, so the linker can compose detail
	// strings without knowing how the caller renders them.
	Val(s string) string
}

Reporter renders a link's progress. The CLI backs it with its feedback layer; a daemon or an assistant backs it with plain lines or nothing at all.

type Result

type Result struct {
	Plan *Plan
	// Site is the registration as written, which can differ from the plan's
	// when a newly installed PHP version was accepted.
	Site config.Site
	// WroteIDEDataSource records that a JetBrains data source was written.
	WroteIDEDataSource bool
}

Result is what a link did.

func Apply

func Apply(plan *Plan, p Policy, d Deps, r Reporter) (*Result, error)

Apply carries out a plan: it registers the site, provisions the runtime that serves it, and performs the project writes the policy allows. A plan that skips registers nothing and returns without error.

func (*Result) Registered

func (r *Result) Registered() bool

Registered reports whether a site was actually registered.

type Skip

type Skip string

Skip says why a directory will not be registered.

const (
	SkipNone       Skip = ""
	SkipWorktree   Skip = "worktree"
	SkipRegistered Skip = "already-registered"
)

type Step

type Step interface {
	OK(detail string)
	Fail(err error)
}

Step is one unit of announced work, closed by exactly one of its methods.

type TextReporter

type TextReporter struct{ W io.Writer }

TextReporter writes plain lines, for the watcher and other log-only callers.

func (TextReporter) Line

func (t TextReporter) Line(msg string)

func (TextReporter) Step

func (t TextReporter) Step(label string) Step

func (TextReporter) Val

func (TextReporter) Val(s string) string

func (TextReporter) Warn

func (t TextReporter) Warn(format string, a ...any)

Jump to

Keyboard shortcuts

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