Documentation
¶
Overview ¶
Package packs implements the pack system: pre-built, opinionated implementations that Forge can install into a project. Think of a pack like a Rails generator gem — it adds real, working code for a specific concern (auth, payments, email, etc.).
Index ¶
- Constants
- func IsInstalled(name string, cfg *config.ProjectConfig) bool
- func MissingDependencies(installed []string) []string
- func ParseConfigOverrides(pairs []string) (map[string]any, error)
- func ResolveInstallOrder(requested []string, existingInstalled []string) ([]string, error)
- func SortInstalledByDependencies(installed []string) ([]string, error)
- func ValidPackName(name string) bool
- type InstallResult
- type Manager
- type Pack
- func (p *Pack) EffectiveKind() string
- func (p *Pack) Install(ctx context.Context, projectDir string, cfg *config.ProjectConfig) (*InstallResult, error)
- func (p *Pack) InstallWithConfig(ctx context.Context, projectDir string, cfg *config.ProjectConfig, ...) (*InstallResult, error)
- func (p *Pack) IsFrontendKind() bool
- func (p *Pack) Remove(projectDir string, cfg *config.ProjectConfig) error
- func (p *Pack) RenderGenerateFiles(projectDir string, cfg *config.ProjectConfig, cs *checksums.FileChecksums) error
- func (p *Pack) SupportsFrontendKind(kind string) bool
- type PackConfig
- type PackFile
- type PackMigration
Constants ¶
const ( // PackKindGo is the default — Go code under pkg/, Go module deps. PackKindGo = "go" // PackKindFrontend installs TypeScript/React assets under // frontends/<name>/ and adds npm dependencies. Files and output // paths are templated against each frontend in the project. PackKindFrontend = "frontend" )
PackKind identifies the language/runtime a pack targets.
Variables ¶
This section is empty.
Functions ¶
func IsInstalled ¶
func IsInstalled(name string, cfg *config.ProjectConfig) bool
IsInstalled checks whether a pack is in the installed list.
func MissingDependencies ¶
MissingDependencies returns the names of packs that the listed installed packs declare in `depends_on` but which are NOT in `installed`. Used by `forge audit` to surface "pack graph health" issues — e.g. someone hand-edited cfg.Packs to remove audit-log while leaving api-key.
Unknown packs are skipped silently. The result is deduplicated and sorted for stable output.
func ParseConfigOverrides ¶
ParseConfigOverrides parses `key=value` strings (typically from a CLI `--config` flag) into a config map. Bare booleans/integers are passed through as strings — the templates can coerce as needed. Returns an error on a missing `=` separator or empty key.
func ResolveInstallOrder ¶
ResolveInstallOrder takes a set of pack names the user wants installed (or that are already installed) and returns those names PLUS any transitive `depends_on` packs in topological order — producers first, consumers last. Names that are already in `existingInstalled` are preserved at the head of the returned slice (existing order respected) and any new transitive deps surface AFTER them but BEFORE the requested-but-not-yet-installed packs.
Returns an error on:
- unknown pack name (typo / pack removed from forge)
- dependency cycle (pack-author bug — surfaces the cycle path)
`requested` may include packs that are already in `existingInstalled`; the result deduplicates. Caller is responsible for skipping the install-side effects on already-installed packs (resync mode does this naturally).
func SortInstalledByDependencies ¶
SortInstalledByDependencies returns the input pack names in dependency-respecting order: producers (depended-on) before consumers. Used by `forge generate` so pack generate hooks run in the right order when one pack's hook references another pack's generated output.
Unknown pack names are silently dropped (matching InstalledPacks's "warn-and-continue" semantics — a pack removed from forge but still listed in cfg.Packs is a known soft failure mode).
func ValidPackName ¶
ValidPackName checks that a pack name contains only safe characters. Delegates to installkit.ValidSlug so the same character class governs packs, starters, and any future installable thing.
Types ¶
type InstallResult ¶
type InstallResult struct {
// PendingProtoGenerate is set true when the install emitted (or
// previously emitted but did not yet render) a `.proto` file that
// the project's `buf generate` / `forge generate` pipeline has NOT
// yet wired into `buf.yaml` and `gen/`. The CLI uses this to print
// a "run `forge generate` to compile new proto definitions" hint
// at the tail of the install so the user isn't left in a
// half-installed state with broken `go mod tidy`.
//
// Pack templates that contribute `.proto` files also import the
// not-yet-generated `gen/<ns>/v1` package, so tidy is intentionally
// deferred to the post-`forge generate` run — this field is the
// signal that the deferral happened and the user must take action.
PendingProtoGenerate bool
}
InstallResult is the structured side-channel returned by Install / InstallWithConfig so the CLI can surface user-facing follow-ups after the install completes. Today there is one signal — PendingProtoGenerate — but the struct shape lets future flags land additively without churning every caller's signature.
type Manager ¶
type Manager interface {
Load(ctx context.Context, name string) (*Pack, error)
Install(ctx context.Context, pack *Pack, projectDir string, cfg *config.ProjectConfig) error
Remove(ctx context.Context, pack *Pack, projectDir string, cfg *config.ProjectConfig) error
List(ctx context.Context) ([]Pack, error)
InstalledPacks(ctx context.Context, cfg *config.ProjectConfig) ([]*Pack, error)
IsInstalled(ctx context.Context, name string, cfg *config.ProjectConfig) bool
RenderGenerateFiles(ctx context.Context, pack *Pack, projectDir string, cfg *config.ProjectConfig) error
}
Manager defines the contract for pack operations: loading, installing, removing, and listing packs within a Forge project.
type Pack ¶
type Pack struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
Version string `yaml:"version"`
// Kind selects the language/runtime the pack targets. "go" (default —
// or empty for backward compatibility) installs Go files under pkg/ and
// runs `go get` / `go mod tidy`. "frontend" installs TypeScript/React
// assets under each frontends/<name>/ directory and runs `npm install`
// in those directories. See PackKindGo, PackKindFrontend.
Kind string `yaml:"kind"`
// Subpath is an informational hint declaring where, under pkg/, the pack
// prefers its non-proto/non-migration code to live. It documents the pack's
// chosen organization (e.g. "middleware/auth/jwtauth") and is surfaced by
// `forge pack info` so a user can see at a glance what subtree the pack
// touches. Forge does NOT enforce categories or matrix rules — output
// paths in `files:` and `generate:` are the source of truth. If omitted,
// the pack is treated as living at the top level under pkg/.
//
// For frontend packs, Subpath is informational and describes the path
// under each frontends/<name>/ directory (e.g. "src/components/data-table").
Subpath string `yaml:"subpath"`
Config PackConfig `yaml:"config"`
Files []PackFile `yaml:"files"`
Dependencies []string `yaml:"dependencies"`
// NPMDependencies lists npm package specs (`name` or `name@version`)
// installed via `npm install` into each frontend directory. Only
// honoured when Kind == "frontend".
NPMDependencies []string `yaml:"npm_dependencies"`
// ProviderNPMDependencies pulls extra npm deps in keyed by the value of
// `pack_config.provider`. Lets a single frontend pack ship variant-specific
// SDKs (e.g. `@clerk/nextjs` for `provider=clerk`, `firebase` for
// `provider=firebase-auth`) without forcing every install to pay for them.
// Only honoured when Kind == "frontend".
ProviderNPMDependencies map[string][]string `yaml:"provider_npm_dependencies"`
// AllowedThirdParty is the per-pack opt-out for the frontendpacklint
// soft rule that flags pack templates importing third-party UI libs
// (@radix-ui/*, @headlessui/*, @tanstack/react-table, ...). Each entry
// is a package prefix that this pack legitimately needs to wrap
// (e.g. "@tanstack/react-table" — a headless engine forge wraps with
// base library primitives, or "recharts" for a charts pack).
// Only honoured when Kind == "frontend".
AllowedThirdParty []string `yaml:"allowed_third_party"`
// SupportsKinds restricts a frontend pack to specific frontend kinds
// (one of "web", "mobile", "vite-spa"). Empty (default) means the pack
// supports all kinds. Most legacy frontend packs are Next.js-coded
// (App Router paths, `next/navigation` imports) and should declare
// `supports_kinds: [web]` until their templates are adapted to the
// other kinds.
//
// Honoured only when Kind == "frontend". On install, forge errors out
// if any of the project's frontends declares a kind not in this list,
// listing the unsupported frontends and the supported set.
SupportsKinds []string `yaml:"supports_kinds,omitempty"`
Generate []PackFile `yaml:"generate"`
Migrations []PackMigration `yaml:"migrations"`
// DependsOn lists the names of OTHER PACKS this pack requires to be
// installed first. Distinct from Dependencies (Go module deps) and
// NPMDependencies (npm package deps): DependsOn captures pack-to-pack
// ordering — e.g. api-key depends on audit-log because the api-key
// generate hook writes audit entries through the audit_events table
// that audit-log creates. Forge topologically sorts at install time
// (auto-installing transitive deps) and at generate time (so consumer
// generate hooks run after producer hooks).
//
// Cycle detection is the loader's responsibility — a cycle is a pack
// authoring bug, not a project bug, so we surface it loudly. Empty
// for the common case (most packs are leaves with no pack-to-pack
// ordering need).
DependsOn []string `yaml:"depends_on,omitempty"`
// PostInstall is a human-facing "next steps" block the CLI prints
// after a successful install — the exact wiring the user must do by
// hand (call sites, interceptor chains, env vars). Packs that install
// code with zero call sites (e.g. jwt-auth's Init/Interceptor) MUST
// say so here: silently shipping unwired code is how users end up
// believing a pack is active when it isn't.
PostInstall string `yaml:"post_install,omitempty"`
}
Pack represents a loadable pack with its manifest and embedded templates.
func ListPacks ¶
ListPacks returns all available packs by scanning the embedded FS for directories containing a pack.yaml manifest.
func (*Pack) EffectiveKind ¶
EffectiveKind returns the pack kind, defaulting to "go" so that legacy pack manifests without a kind field continue to behave as Go packs.
func (*Pack) Install ¶
func (p *Pack) Install(ctx context.Context, projectDir string, cfg *config.ProjectConfig) (*InstallResult, error)
Install renders and writes pack files into the project, adds dependencies, and records the pack in forge.yaml. Behaviour branches on EffectiveKind — Go packs run `go get`/`go mod tidy`, frontend packs iterate over each project frontend and run `npm install` per frontend.
Equivalent to InstallWithConfig(projectDir, cfg, nil).
func (*Pack) InstallWithConfig ¶
func (p *Pack) InstallWithConfig(ctx context.Context, projectDir string, cfg *config.ProjectConfig, overrides map[string]any) (*InstallResult, error)
InstallWithConfig is Install with per-install config overrides. Overrides are merged on top of the pack's `config.defaults` block before templates are rendered, so users can pass e.g. `--config provider=clerk` to pick a variant exposed by the pack templates as `{{ .PackConfig.provider }}`.
Overrides are surfaced to templates via the standard PackConfig data key. Unknown keys are accepted (the pack's templates decide whether to honour them) — validation is the pack author's responsibility.
Idempotency: a re-install (pack already listed in cfg.Packs) operates in resync mode — files with overwrite=once that already exist are skipped, and migrations whose slug already lives in db/migrations/ are skipped rather than re-allocated under a fresh sequential ID. Surfacing both as "skipping" notes lets `forge pack install <name>` be safely re-run after a partial-failure or after the pack ships a new file the project lacks.
Collision safety: for a fresh install (pack not yet in cfg.Packs), if a pack file with overwrite=once would land on an existing file the pack did not previously emit, the install fails fast with a rename recipe. This catches the case where a pack ships a service handler/proto whose name the user has already scaffolded — a silent skip would yield a build that still references the user's version while the pack thinks it installed.
func (*Pack) IsFrontendKind ¶
IsFrontendKind reports whether the pack targets a frontend (TypeScript/React).
func (*Pack) Remove ¶
func (p *Pack) Remove(projectDir string, cfg *config.ProjectConfig) error
Remove deletes files created by the pack and removes it from the project config. Dependencies (go modules or npm packages) are left in place since they may be used by other code.
func (*Pack) RenderGenerateFiles ¶
func (p *Pack) RenderGenerateFiles(projectDir string, cfg *config.ProjectConfig, cs *checksums.FileChecksums) error
RenderGenerateFiles re-renders the pack's generate-hook templates. Called during `forge generate` to keep pack-generated code up to date.
Generate-hook files are Tier-1 output: they carry the "Code generated by forge … regenerated every run" banner and are re-emitted on every run, exactly like core codegen. They are therefore written through the checksum-tracked Tier-1 writer (checksums.WriteGeneratedFile via renderTrackedGenerateFile) so each one is self-certifying — it gets a forge:hash marker, is recorded as written-this-run (RestampWritten re-certifies it after goimports), and gains stomp-guard protection. This is what keeps a pack-emitted _gen.go (e.g. the audit-log pack's interceptor_gen.go) out of the audit's orphan_gen_files bucket. A nil cs falls back to the plain untracked write (callers outside the generate pipeline that have no checksum ledger).
func (*Pack) SupportsFrontendKind ¶
SupportsFrontendKind reports whether the pack's manifest declares support for the given frontend kind. An empty SupportsKinds list means the pack supports every kind (default for backward compatibility).
The empty/unspecified kind ("") is treated as "web" — that's the legacy default Next.js kind used by `forge new` and `forge add frontend` when no --kind flag is passed.
type PackConfig ¶
type PackConfig struct {
Section string `yaml:"section"`
Defaults map[string]any `yaml:"defaults"`
}
PackConfig describes the configuration section a pack adds to forge.yaml.
type PackFile ¶
type PackFile struct {
Template string `yaml:"template"`
Output string `yaml:"output"`
Overwrite string `yaml:"overwrite"` // "always" | "once" | "never"
Description string `yaml:"description"` // optional human description
}
PackFile describes a single template→output file mapping.
type PackMigration ¶
type PackMigration struct {
// Name is the slug appended after the allocated ID (e.g. "api_keys" →
// "00002_api_keys.up.sql"). Required.
Name string `yaml:"name"`
// Up is the template that renders the up-migration SQL. Required.
Up string `yaml:"up"`
// Down is the template that renders the down-migration SQL. Required.
Down string `yaml:"down"`
// Description is an optional human description.
Description string `yaml:"description"`
}
PackMigration describes a single migration that the pack contributes to db/migrations/. The migration ID (numeric prefix) is allocated at install time by scanning existing migrations — this avoids hardcoded IDs colliding across packs and keeps zero-padding consistent with the scaffold (5 digits).