This document describes the architectural design, component interactions, interface abstractions, and command boundaries of the AetherPak CLI toolchain.
AetherPak CLI is a foundation-first Go toolchain designed to compile, lint, package, sign, push (to OCI registries), and publish (to static hosting/GitHub Pages) Flatpak applications.
The application architecture is structured to separate the Command-Line Interface Boundary (cmd/) from the Core Business Logic (pkg/). This decoupling ensures that the core features remain testable, reusable, and free from hard OS-level side effects (like direct os.Exit calls).
graph TD
CLI[cmd/ CLI Entrypoint] --> Config[pkg/config]
CLI --> Executor[pkg/executil]
subgraph Core Logic [pkg/]
Builder[pkg/builder] --> Executor
Importer[pkg/importer] --> Executor
OCI[pkg/oci] --> Executor
Site[pkg/site]
Plan[pkg/plan]
Signing[pkg/signing]
Record[pkg/record]
end
CLI --> Builder
CLI --> Importer
CLI --> OCI
CLI --> Site
CLI --> Plan
CLI --> Signing
Following the Git design philosophy, the CLI divides its commands into Porcelain (user-facing, high-level workflows) and Plumbing (low-level, modular utilities).
Porcelain commands orchestrate multiple plumbing modules to provide a seamless user experience.
-
aetherpak add: Creates or modifiesaetherpak.yaml, adding one app from one of three sources:- A local manifest path (app id derived from the manifest, with sensible defaults).
- A bundle URL, downloaded and fingerprinted (SHA-256 recorded).
- A git repository, added as a recursively-initialised submodule with its manifest auto-detected.
It edits the config comment-preservingly, shows a colored diff, and gates on confirmation (interactive wizard on a TTY, or
--confirm/-yto skip). Git submodule additions are rolled back if the user declines. -
aetherpak build: Compiles flatpak manifests. It automatically:- Installs/resolves
flatpak-builder-lint. - Runs pre-build linting on the manifest.
- Executes
flatpak-builderwith CCache/state management. - Runs post-build linting on the generated repository.
- Installs/resolves
-
aetherpak import: Downloads and imports an external.flatpakbundle. It handles:- Downloading/verifying SHA-256 checksums.
- Importing the bundle into a scratch repository.
- Rebinding the branch commit ref to match the target release channel.
-
aetherpak publish: Aggregates records, reconciles remote OCI images, signs assets, and generates static pages. -
aetherpak release: A comprehensive, single-app workflow that runs build, push-oci, and publish steps in sequence. -
aetherpak preview: Generates and serves a local landing page template preview using customizable mock/dummy data or a live production index, simulating GPG status, single/multiple apps, and template rendering. -
aetherpak clean: Clears builder compiler caches, state directories, template preview files, production site outputs, build-site records, and local OSTree repositories. Requires user confirmation when run interactively.
Plumbing commands are designed to be run programmatically (e.g., inside CI matrix runners or custom scripts) and perform single, deterministic tasks.
aetherpak push-oci: Packages a local OSTree repository ref into an OCI container image and pushes it to a registry.aetherpak build-site: Reads a tree of cell execution records, fetches the active landing page index, merges new records, and writes static Pages assets.aetherpak plan: Analyzes repository git differences since a base commit and expands theaetherpak.yamlconfiguration into a parallelizable build/publish matrix.aetherpak inspect-repo: Resolves information (commits, metadata, refs) directly from a local OSTree repository.
To achieve high unit test coverage without invoking real shell binaries (like flatpak or ostree), the CLI abstracts command execution and configuration injection.
Instead of calling the standard library's exec.Command directly, all subprocess execution is routed through the Executor and Command interfaces:
type Command interface {
Run() error
Start() error
Wait() error
StdoutPipe() (io.ReadCloser, error)
StderrPipe() (io.ReadCloser, error)
SetEnv(env []string)
SetStdout(w io.Writer)
SetStderr(w io.Writer)
}
type Executor interface {
Command(name string, arg ...string) Command
LookPath(file string) (string, error)
}OSExecutor: Production implementation wrapping Go'sos/exec.MockExecutor/MockCommand: Mock implementations configured with hooks (OnCommand) and predefined stdout/stderr outputs to assert command arguments and simulate execution results during tests.
The StreamWithPrefix function reads output from subprocess pipes line-by-line and writes them with formatted prefixes (e.g., flatpak-builder |) to standard output. It uses a chunk-based loop:
- It reads bytes until
\nusingbufio.Reader.ReadBytes. - It accumulates partial lines when encountering buffer-boundary errors (
bufio.ErrBufferFull). - It flushes any remaining trailing bytes on EOF/error.
- It bypasses prepending a leading space when the prefix is empty (
"").
- Responsible for parsing, normalizing, and validating the
aetherpak.yamlworkspace configuration. - Asserts structural requirements (e.g., ensuring an app has either a manifest or a bundle URL, checking that app IDs follow reverse-DNS patterns, and preventing path-traversals).
- Supports
builder_argslist parameters at both the globaldefaultslevel and the per-app level (facilitating inheritance of build arguments).
- Wraps the
flatpak-builderruntime compilation workflow. - Launches asynchronous goroutines to stream build output.
- Accepts extra passthrough arguments (
BuilderArgs) from configuration and CLI overrides to customize the execution of the compilation sandbox. - Explicitly cleans up/closes stdout and stderr pipe file descriptors after streaming, ensuring no leaks in long-running processes.
- Implements a robust 5-step import pipeline:
- Download: Fetches external bundles and validates SHA-256 integrity.
- Scratch OSTree Init: Initializes a temporary OSTree archive repository.
- Build Import: Invokes
flatpak build-import-bundleto import the.flatpakfile. - Ref Resolution: Lists refs in the scratch repository to resolve the bundle's application ID.
- Branch Rebinding: Invokes
flatpak build-commit-fromto copy the ref into the target repository while re-mapping it to the consumer-defined channel (e.g., rebindingapp/org.example.App/x86_64/mastertoapp/org.example.App/x86_64/stable).
- Exposes a reusable
Fetch(url, progress)helper (download to a temp file + SHA-256, with a progress callback) shared byimportandadd.
- Orchestrates the
aetherpak addworkflow: resolves a manifest/bundle/git source into aconfig.App, validates it, edits the config (comment-preserving), renders a diff, gates on confirmation, then writes or rolls back side effects. - All filesystem, git, and network IO is injected, keeping the orchestration unit-testable.
options.gois a DRY registry of boolean add options (run-linter,ccache,install-deps-from-flathub) — a single source of truth that drives the wizard checklist, the CLI flags, and the tooltips; adding an entry surfaces a new checkbox + flag automatically. Builder-affecting options are skipped for prebuilt bundles. The app id is auto-detected from the manifest (never prompted); the git source only prompts for the manifest path when auto-detection fails.
- Parses Flatpak manifests (JSON or YAML) to extract the application id, and detects the single manifest within a directory (
DetectInDir). Shared byplanandadd.
- A small, reusable abstraction over git (submodule add/update/remove plus
rev-parse,cat-file,show,diff), backed bypkg/executilfor testability. TheGitinterface is the seam intended to be re-implemented with a native Go git library later without touching callers;pkg/planandpkg/adderboth consume it.
- Performs comment-preserving edits of
aetherpak.yamlby operating on theyaml.v3node tree (appending an app to theappssequence) rather than re-marshaling the whole struct, so existing comments and ordering survive.
- Renders a unified, lipgloss-colored diff between two config blobs (using
go-udiff), with a plain mode for non-TTY output.
- Bundles OSTree repositories into OCI images using
flatpak build-bundle. - Interacts with container registries using the
go-containerregistrylibrary. - Generates an image manifest index and signs the manifest payload. Supports bypassing GPG signing when
no_signis set totrue. Whenno_signisfalse, it enforces that a GPG key must be provided, failing the push unlessallow_unsignedis explicitly enabled. - Writes a uniform execution record (
record.json) under<records-dir>/<app-id>-<arch>/tracking the OCI digest, registry, and label metadata.
- Assembles the static repository landing page.
- Fetches the current production index from the active Pages hosting to seed the update.
- Merges execution records from parallel runner cells.
- Reconciles the index by validating digest existence via registry
HEADchecks (pruning entries only on definitive 404s). - Generates GPG public key material (
key.asc), signing manifests (signing.json),.flatpakrepoconfigurations, and one-click.flatpakrefinstaller files. Bypasses key export and GPG validation checks whenno_signis set totrue, and enforces GPG key existence unlessallow_unsignedis explicitly enabled.
- Compares Git trees (
git diff) since a base SHA to detect changes in manifests, directories, and configuration settings. - Expands active configs into a parallel workflow matrix, optimizing CI resources by building only modified apps/architectures.
- Encapsulates GPG simple-signing helper functions.
- Manages GPG private key loading, public key ring exports, and payload signatures.
The CLI implements strict error-handling safety:
-
Decoupled Process Exiting: Subcommands are defined using Cobra's
RunEpattern. Instead of callingos.Exitdirectly inside cmd packages, subcommands return errors. This guarantees that Go's deferred functions (such as temp directory cleanup and socket closes) run completely. -
CmdErrorStruct: A customCmdErrortype wraps internal errors with an associated process exit code:type CmdError struct { ExitCode int Err error }
- Implements
Unwrap() errorto preserve error chains. cmd.Execute()captures this error at the boundary, prints a formatted error banner, and returns the exit code tomain.gowhereos.Exit()is invoked.
- Implements