Using the Go API πΉ¶
airfs is a Go SDK first; the command line is a thin frontend over it. A program
that needs the merged view consumes it in process, with no mount, no
subprocess, and nothing to parse.
There is deliberately no machine-readable output mode on the CLI. A second, stringly-typed interface for the same data would be a contract to keep stable for no caller that could not do better than parsing prose.
The merged view is an fs.FS π¶
layerfs.FS implements fs.FS, fs.ReadDirFS, and fs.StatFS, so everything
in the standard library that consumes a filesystem already works β fs.WalkDir,
fs.ReadFile, template.ParseFS, http.FS.
cfg, err := sources.Load(configPath)
if err != nil {
return err
}
skills := cfg.Merged(airfs.Skills) // *layerfs.FS
data, err := fs.ReadFile(skills, "commit/SKILL.md")
Merged gives the union for one kind; airfs.Kinds iterates all four
(airfs.Agents, airfs.Skills, airfs.Commands, airfs.Scripts).
An FS is immutable once constructed and safe for concurrent use, and it holds
no cache of any kind β an edit inside a layer is visible through it immediately.
Changing the set of layers means constructing a new one.
Building a union directly π§©¶
You do not need a config file, or even a disk. layerfs.New takes any fs.FS,
last winning β which is what makes merge behaviour testable without mounting
anything:
merged := layerfs.New(
layerfs.Layer{Name: "personal", FS: os.DirFS(personalSkills)},
layerfs.Layer{Name: "project", FS: fstest.MapFS{
"commit/SKILL.md": {Data: []byte("project version")},
}},
)
Layer.Root is the real directory backing FS, when there is one; it is empty
for an in-memory layer. The union never uses it β only a frontend that wants to
read through to the actual file does.
Asking where an entry came from π¶
Origin and the directory listing derive from the same index, so a name you
listed and a name you looked up always resolve to the same layer.
Reporting shadowing π΅οΈ¶
shadows, err := merged.Shadowed()
for _, s := range shadows {
log.Printf("%s: %s wins over %d other layer(s)", s.Name, s.Winner.Name, len(s.Losers))
}
This is what makes precedence auditable from your own tooling, in the same terms
airfs sources prints.
Mounting from Go π§΅¶
if err := mount.Preflight(); err != nil {
return err // /dev/fuse or a setuid fusermount3 is missing
}
server, err := mount.Serve(target, cfg)
if err != nil {
return err
}
defer server.Unmount()
server.Wait() // blocks until unmounted
Serve establishes every kind together and releases them all if any fails β a
partially mounted target is a view that lies about what is available.
For inspection without serving, mount.Status(target) returns one State per
kind, read from the kernel's mount table. A State that is Mounted and
Stale is the signature of a serving process that died: still listed, failing
every access. mount.Served(states) collapses the four into the one boolean most
callers want.
mount.Requirements() gives the same host check airfs doctor prints, as data β
each with Name, Satisfied, Detail, and ProvidedBy.
Errors you should branch on β οΈ¶
Failures that mean the host or the configuration needs attention β rather than
airfs malfunctioned β are marked as preconditions:
if airfs.IsPrecondition(err) {
// missing config, unresolvable layer, non-empty mountpoint,
// absent mount prerequisite β tell the user what to fix
}
It unwraps, so it works through your own fmt.Errorf("%w") wrapping. This is the
same distinction the CLI turns into exit code 2.
Package map πΊοΈ¶
Everything importable lives under sdk/; cmd/airfs is the frontend and is not
part of the API. The sdk directory holds package airfs, so an import of
.../airfs/sdk is used as airfs.Kind, airfs.IsPrecondition, and so on.
| Package | Holds |
|---|---|
github.com/sylvanld/airfs/sdk |
Package airfs: the model (Kind, Kinds), the precondition contract, exit codes. |
.../sdk/layerfs |
The ordered read-only union, as a standard fs.FS. |
.../sdk/sources |
Reading and resolving sources.txt into layers. |
.../sdk/mount |
Exposing a view at a real path via FUSE, and reading mount state. |
Every dependency is cgo-free, so a program embedding airfs still builds with
CGO_ENABLED=0.