bundles

package
v0.3.2 Latest Latest
Warning

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

Go to latest
Published: Aug 7, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

Documentation

Overview

Package bundles installs bundles: packages that group an application, its workflows, and its plugin dependencies into one .patchcord-bundle archive (vision document, section 9.3). A bundle install delegates to internal/apps and internal/runs rather than duplicating their logic — this package only orchestrates, in order: checking declared plugin dependencies are present, installing the embedded app (if any), and installing the embedded workflows.

Embedding connectors ("configuration" in the vision document's wording) is not modeled yet: internal/connectors has no file-based export/template mechanism today (ADR-0020), so there is nothing a bundle could portably carry beyond a non-secret connector's id and type — deferred to a later pass, not forgotten.

Index

Constants

View Source
const ManifestFileName = "bundle.yaml"

ManifestFileName is the file a bundle's source/staging directory must contain at its root.

View Source
const PackageExtension = ".patchcord-bundle"

PackageExtension is the conventional file extension for a bundle package produced by Pack (vision document, section 9.3).

Variables

View Source
var ErrInvalidManifest = errors.New("invalid bundle manifest")

ErrInvalidManifest is returned by ParseManifest and LoadManifest when the manifest is malformed or missing a required field.

View Source
var ErrNotFound = errors.New("bundle not found")

ErrNotFound is returned by Get when no bundle with the given id has been recorded.

Functions

func Pack

func Pack(sourceDir string, key ed25519.PrivateKey, w io.Writer) error

Pack archives sourceDir's bundle.yaml plus exactly the app/workflow files it references into w as a gzip-compressed tar stream, plus a checksums.json covering it (see internal/packaging.SignedArchive). If key is non-nil, the package is also signed — signing a bundle covers its embedded app and workflows too, so they are never separately verified again on install (see installEmbeddedApp). The result is what InstallPackage (and therefore `patchcord bundle install`) expects.

If the manifest declares an app, its patchcord-app.yaml is validated before anything is staged — the same check apps.Pack itself runs. Without this, a Vite app built without its manifest under public/ (an easy step to forget, since Vite copies public/ into the build output verbatim) packs successfully and only fails at `bundle install` time, deep inside an ephemeral staging directory whose path means nothing to whoever is installing the package.

Pack never walks sourceDir itself: only the manifest's declared App subtree and Workflows files are staged and archived. sourceDir routinely holds things bundle.yaml does not declare — a Vite app's node_modules (which packaging.Archive would otherwise reject outright the moment it hit a symlink such as node_modules/.bin/esbuild), a .git directory, editor state — none of which belong in the package.

func Scaffold

func Scaffold(dir, id, version string) error

Scaffold writes a minimal bundle.yaml and AGENTS.md to dir, plus an embedded app (delegating to apps.Scaffold) and workflow (delegating to workflow.Scaffold) so the result is `bundle pack`-able as-is — requires_plugins starts empty since Scaffold has no way to know what plugin the bundle should depend on. It returns an error if dir already exists and is not empty.

func ScaffoldVite

func ScaffoldVite(dir, id, version string) error

ScaffoldVite writes a bundle.yaml to dir, plus an embedded Vite + TypeScript project (delegating to apps.ScaffoldVite) and workflow (delegating to workflow.Scaffold). Unlike Scaffold, the result is not `bundle pack`-able as-is: the embedded app's patchcord-app.yaml only exists at app/dist after building it —

cd dir/app && npm install && npm run build

bundle.yaml's app field already points at app/dist, so `bundle pack`/`bundle dev [--watch]` need nothing further once that build has run at least once. It returns an error if dir already exists and is not empty.

Types

type Bundle

type Bundle struct {
	ID          string
	Version     string
	Manifest    string // raw bundle.yaml source
	InstalledAt time.Time
}

Bundle is one installed bundle's provenance record: what was declared, not a second source of truth for the app/workflows it installed (see the package doc comment).

func Get

func Get(ctx context.Context, db *sql.DB, id string) (*Bundle, error)

Get returns one installed bundle's provenance record by id. It returns ErrNotFound if no bundle with that id has been recorded.

func InstallDir

func InstallDir(ctx context.Context, db *sql.DB, dir string, knownActions map[string]workflow.KnownAction) (*Bundle, error)

InstallDir installs a bundle straight from a source directory instead of a packaged .patchcord-bundle archive — no Pack/Extract round trip, no checksum, no signature: dir is local, unsigned-by-design source under active development. It backs `patchcord bundle dev` the same way apps.InstallOrUpdate backs `patchcord app dev`.

Unlike InstallPackage's installEmbeddedApp, the embedded app (if any) is installed in place via apps.InstallOrUpdate pointed straight at dir/manifest.App — never moved under dataDir/apps — so it ends up served live off dir exactly as `app dev` serves an app: rebuilding it (e.g. `vite build --watch`) needs no further agent involvement, and no dataDir argument is needed here at all.

Each embedded workflow goes through installWorkflowForDev (ADR-0055), not the strict installWorkflowIfChanged InstallPackage uses: redeclaring an already-installed version with byte-identical content is a silent no-op (as before, and for the same reason — `bundle dev --watch` reinstalls every embedded workflow on every change under dir, including ones a given save never touched), but redeclaring it with genuinely different content — edited the workflow's body, forgot to bump `version:` — is installed under the next unused version instead of rejected, so a save is never a hard failure. The source file is never rewritten; ADR-0008 immutability stays intact for InstallPackage (`bundle install`/`update`) and `workflow install`, which are unaffected by this.

requires_plugins is enforced exactly as InstallPackage enforces it: a missing dependency is not installed automatically.

func InstallPackage

func InstallPackage(ctx context.Context, db *sql.DB, dataDir, packagePath string, knownActions map[string]workflow.KnownAction, requireSignature bool) (*Bundle, trust.PolicyResult, error)

InstallPackage installs a bundle from a .patchcord-bundle archive (Pack's output). It orchestrates, in order:

  1. every "id@version" entry in requires_plugins must already be present in the plugin catalog — a bundle never auto-installs its plugin dependencies (see ADR-0044: this stays deferred even once a registry exists);
  2. the embedded app (if any) is moved to its permanent location under dataDir/apps/<app-id>/<app-version> and installed via apps.InstallOrUpdate — unlike apps.InstallPackage's own standalone .patchcord-app flow (which stays strict, see apps.Install), a bundle install has always been upsert-by-design at the top level (record(), below): re-running `bundle install`/`bundle update` on an already-installed bundle id must succeed and replace its embedded app in place, not fail with apps.ErrAlreadyExists (ADR-0044);
  3. each embedded workflow file is installed via installWorkflowIfChanged — workflow definitions need no on-disk home of their own, they live in the workflow_versions table (ADR-0008), and re-running install/ update with an unchanged workflow is a no-op rather than a rejection (only redeclaring a version with genuinely different content still hits ADR-0008's immutability rule).

The package is verified (internal/packaging.Verify) before anything is installed: a checksum mismatch or an invalid signature aborts unconditionally. requireSignature additionally rejects a package that is unsigned, or signed by a key not trusted for its id (internal/trust) — when false, InstallPackage still returns the verification outcome so the caller can warn about either case instead of failing outright. A bundle's signature covers its embedded app and workflows too — they are not separately re-verified (see installEmbeddedApp).

A failure partway through (e.g. the app installs but a workflow fails validation) is not rolled back: this first pass does not implement multi-resource transactions across three independently-catalogued resource kinds. The returned error names which step failed.

func List

func List(ctx context.Context, db *sql.DB) ([]Bundle, error)

List returns every installed bundle's provenance record, ordered by id.

type Manifest

type Manifest struct {
	ID        string
	Version   string
	App       string   // relative path to the embedded app's source directory; empty if the bundle has no app.
	Workflows []string // relative paths to embedded workflow YAML files.
	// RequiresPlugins lists "id@version" plugin dependencies InstallPackage
	// checks are already installed before proceeding — see the package doc
	// comment: v1 validates, it does not auto-install.
	RequiresPlugins []string
}

Manifest is the parsed content of a bundle's bundle.yaml.

func LoadManifest

func LoadManifest(dir string) (*Manifest, error)

LoadManifest reads and parses dir's bundle.yaml.

func ParseManifest

func ParseManifest(source []byte) (*Manifest, error)

ParseManifest parses and validates a bundle manifest from its YAML source, returning ErrInvalidManifest if a required field is missing, empty, or malformed.

Jump to

Keyboard shortcuts

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