Documentation
¶
Overview ¶
Package component is fft's extension point: a way for anyone to add a building block to the CLI without a pull request into this repository.
What a component is ¶
A directory holding a manifest and an executable:
<root>/emulator/ component.yaml bin/fft-emulator
fft reads the manifest at startup, registers a cobra command for everything it declares, and — when one of those commands is run — executes the binary with the remaining arguments and an environment it builds itself. The child inherits stdin, stdout and stderr, so the output contract is passed straight through, and its exit code becomes fft's.
The environment is the interface ¶
A component is a *headless fft consumer*. It never opens the keychain and never reads the config file; it receives the FFT_* variables config.FromEnv already defines, which is the same contract a CI job uses. That is why this package can be small: the handover was designed before the extension point was.
What it hands over is decided by the session level the manifest declares, and Environ is the only place that decision is made — see its documentation for why a component never receives the Firebase API key.
No PATH scanning ¶
Unlike kubectl, an executable called fft-something on $PATH is *not* a command. The tree comes from the compiled-in first-party table and from the managed root, and from nowhere else, so what `fft --help` prints never depends on what else is installed on the machine. Same instinct as the read-only gate: an invariant that says the surprising thing cannot happen is worth more than a warning that it did.
Trust ¶
A component is code, and it runs as the user who ran fft. The manifest *declares* intent — whether a command mutates the tenant, which session it needs — and fft enforces what it can: it refuses a mutating component against a read-only project, and it never exports a credential the declared session did not ask for. It cannot stop a component that lies. `fft component install` says so, in those words, before it writes anything.
Index ¶
- Constants
- func DefaultLang(kind Kind) string
- func Environ(base []string, c Component, cmd Command, opts EnvOptions) ([]string, error)
- func Root(lookup func(string) (string, bool)) (string, bool, error)
- func Run(ctx context.Context, c Component, args []string, env []string, streams Streams) error
- type Command
- type Component
- type EnvOptions
- type ExitError
- type File
- type Installer
- type InstallerOption
- type Kind
- type Manifest
- type NotInstalledError
- type Option
- type Plan
- type Problem
- type Registry
- type Scaffold
- type Session
- type SessionInfo
- type Source
- type Streams
Constants ¶
const ( // EnvAPI is the manifest contract version of the fft that spawned the component, // so a component can refuse a host it does not understand instead of failing in // some more interesting way further in. EnvAPI = "FFT_COMPONENT_API" // EnvName is the component's own name, which is how a binary serving several // components — or one invoked by hand — knows which one it is being. EnvName = "FFT_COMPONENT_NAME" // EnvVersion is the version of fft itself. A component that shells out to fft can // check it; one that does not can ignore it. EnvVersion = "FFT_VERSION" )
Variables fft sets on every component it runs, whatever session it grants.
const ( LangShell = "shell" LangGo = "go" LangPython = "python" LangNode = "node" )
Supported languages a scaffold can be written in.
const APIVersion = 1
APIVersion is the manifest contract this build speaks.
A manifest declaring anything else is refused rather than read leniently: the fields fft would silently ignore are exactly the ones a future version added to change what a component *does*, and quietly running it with half its manifest honoured is worse than not running it at all.
const DefaultRepo = "Joessst-Dev/fft-cli"
DefaultRepo is where a first-party component comes from. A short name — `fft component install emulator` — resolves here.
const EnvRoot = "FFT_COMPONENT_DIR"
EnvRoot overrides where components are installed and looked for.
Set-but-empty is not the same as unset: it means *no components*, and it is how `fft gen-docs` and the specs pin a tree that does not depend on what the machine happens to have installed. Generated documentation that changes because the developer installed something is documentation that fails CI for a reason nobody can see in the diff.
const ManifestName = "component.yaml"
ManifestName is the file that makes a directory a component.
Variables ¶
This section is empty.
Functions ¶
func DefaultLang ¶
DefaultLang is the language a kind is scaffolded in when none is named: a shell script for a command (runs immediately, no toolchain), and Go for a transport (which is what most transport authors want, and the only language with the protocol as a library rather than a wire format to reimplement).
func Environ ¶
Environ builds the environment a component runs with.
This is the whole credential boundary, and it works by subtraction. Every FFT_-prefixed variable is stripped from the inherited environment first, and then exactly what the declared session allows is put back. So a developer with FFT_PASSWORD exported in their shell does not silently hand it to every component they run, and a component cannot reach the tenant by any route the manifest did not ask for. Everything outside the FFT_ namespace — PATH, HOME, PUBSUB_EMULATOR_HOST, a proxy setting — is inherited untouched, because those belong to the machine and not to fft.
base is the environment to inherit, in os.Environ form.
func Root ¶
Root is where components live: $FFT_COMPONENT_DIR, else $XDG_DATA_HOME/fft/components, else ~/.local/share/fft/components.
Data, not config and not cache. A component is neither something the user edits nor something fft can regenerate by asking again, which is what the other two directories mean.
The second return value reports whether components are enabled at all; it is false when EnvRoot is set to the empty string.
Types ¶
type Command ¶
type Command struct {
// Name is the command as the user types it: `fft <name>`.
Name string `yaml:"name" json:"name"`
// Short is the one-line summary in `fft --help`.
Short string `yaml:"short" json:"short"`
// Long is the description `fft <name> --help` prints when the component is not
// installed. Once it is, --help goes to the child, which knows its own flags.
Long string `yaml:"long,omitempty" json:"long,omitempty"`
// Session is how much of the caller's tenant session the command receives.
Session Session `yaml:"session" json:"session"`
// Mutates declares that the command can change the tenant. It is what the
// read-only gate reads: a mutating component command is refused before the child
// is spawned, exactly as a mutating operation is refused before a request is
// built.
//
// It defaults to false, and that is safe *only* because a component that says
// nothing also gets no credential to write with: SessionNone is the zero value
// too. A manifest declaring a write session must declare Mutates as well, or
// validation refuses it — see [validCommand].
Mutates bool `yaml:"mutates,omitempty" json:"mutates"`
// Claims are the operationIds this command supersedes. An operation a component
// claims gets no generated Tier-2 twin, which is what lets the community promote
// an endpoint to a curated UX the same way a hand-written command does.
Claims []string `yaml:"claims,omitempty" json:"claims,omitempty"`
}
Command is one command a component adds to fft's tree.
type Component ¶
type Component struct {
Manifest
// Dir is the component's own directory, absolute.
Dir string
// FirstParty marks a component fft ships and knows about at compile time. Its
// command tree comes from [FirstParty], not from the manifest on disk — see that
// function for why.
FirstParty bool
// Installed reports whether the executable is actually there. A first-party
// component is registered whether or not it is, so that `fft emulator` can
// explain how to get it rather than not existing.
Installed bool
}
Component is one installed component: its manifest, and where it lives.
func (Component) Delivers ¶
Delivers reports whether a transport component handles this subscription target type.
func (Component) ExecPath ¶
ExecPath is the absolute path of the component's executable, with the platform's suffix if that is how it is spelled on disk.
Not called Exec, though that is what the manifest field is called: a method of that name on the embedding struct would shadow the embedded field, and the field is the one an installer has to be able to write.
type EnvOptions ¶
type EnvOptions struct {
// Root is the component root, passed on so a component can find its own
// sub-components — the emulator finds its transports this way.
Root string
// Version is fft's own version.
Version string
// Output is the -o format, NoColor the --no-color flag and Timeout the --timeout
// duration, forwarded so a component that renders anything renders it the way the
// user asked for.
Output string
NoColor bool
Timeout time.Duration
// Session is the resolved tenant session, or nil when there is none. It is
// required for a command declaring anything but [SessionNone].
Session *SessionInfo
// Extra is configuration the host wants to hand the component, by variable name.
//
// Only the names the manifest declares in [Manifest.Env] are actually set, and
// that filter is the point of the field existing at all: the emulator turns
// --pubsub-emulator-host into PUBSUB_EMULATOR_HOST for a transport that says it
// reads it, rather than pushing every flag it has at every child. What a
// component consumes stays something it declared and `fft component info` can
// print.
Extra map[string]string
}
EnvOptions is what Environ needs from the caller that is not the manifest.
type ExitError ¶
type ExitError struct {
// Name is the component's name, for a caller that wants to say which one failed.
Name string
// Code is the status the component exited with.
Code int
}
ExitError is a component that exited non-zero.
It is silent, and that is the whole reason it is a type. The component wrote its own diagnostics to the stderr it inherited — the user has already read them — so fft printing "Error: exit status 6" underneath would be a second, worse message about something already explained. What fft still owes the caller is the exit code, which is the part a script reads.
A component is documented to use fft's own table (internal/exitcode), so a 6 out of a component means the same thing as a 6 out of fft.
type File ¶
type File struct {
// Name is the path relative to the component's own directory, slash-separated.
Name string
// Data is the file's contents.
Data []byte
// Mode is the permission bits to create it with — [execMode] for a bin/ script,
// [fileMode] otherwise.
Mode os.FileMode
}
File is one file a scaffold emits, with the mode it should be written under.
type Installer ¶
type Installer struct {
// contains filtered or unexported fields
}
Installer downloads, verifies and unpacks components.
It reaches GitHub and nothing else, with its own http.Client: the API client carries a bearer token for the tenant, and a release download has no business anywhere near it.
func NewInstaller ¶
func NewInstaller(root string, opts ...InstallerOption) *Installer
NewInstaller returns an Installer writing into root.
The download client is built here and never overridable: a replaceable client would be a way to hand the installer one without the CheckRedirect guard below, and specs reach GitHub through WithAPI instead, which keeps the guard in place.
func (*Installer) Commit ¶
Commit moves a staged component into place, replacing whatever was there.
The swap is a rename of one directory over another, in two steps because that is as atomic as a directory replacement gets: the old tree is moved aside, the new one moved in, and only then is the old one deleted. A crash in the middle leaves the component either installed or not — never half of each — and leaves at worst a directory called <name>.old-… that the next install cleans up.
func (*Installer) Prepare ¶
Prepare downloads and verifies a component, and stages it, without touching the installed tree. Call Installer.Commit to finish, or Installer.Discard to abandon it.
func (*Installer) Remove ¶
Remove uninstalls a component.
It refuses a directory that holds no manifest, for the same reason `fft skill install --force` refuses a directory with no SKILL.md: the name comes from a shell, where a typo is one keystroke, and fft will not recursively delete a directory it cannot prove it put there.
type InstallerOption ¶
type InstallerOption func(*Installer)
InstallerOption configures an Installer.
func WithAPI ¶
func WithAPI(url string) InstallerOption
WithAPI replaces GitHub's endpoint. Specs point it at an httptest server.
type Kind ¶
type Kind string
Kind is what a component extends.
const ( // KindCommand adds subcommands to fft. KindCommand Kind = "command" // KindTransport delivers emulator events to a broker fft has never heard of. // It is addressed by the emulator over a line protocol rather than by the user, // so it declares target types instead of commands. KindTransport Kind = "transport" )
type Manifest ¶
type Manifest struct {
// APIVersion is the contract the component was written against. See [APIVersion].
APIVersion int `yaml:"apiVersion" json:"apiVersion"`
// Name identifies the component and names its directory. It is also the default
// command name, so it is constrained to what is safe as both — see [validName].
Name string `yaml:"name" json:"name"`
// Version is the component's own version, for `fft component list` and for
// deciding whether an upgrade has anything to do. fft does not interpret it.
Version string `yaml:"version" json:"version,omitempty"`
// Description is one line, shown by `fft component list`.
Description string `yaml:"description" json:"description,omitempty"`
// Kind is what the component extends.
Kind Kind `yaml:"kind" json:"kind"`
// Source is where the component came from, recorded at install time so that
// `fft component upgrade` knows where to look and `fft component info` can say
// whose code this is.
Source string `yaml:"source" json:"source,omitempty"`
// Exec is the executable, as a slash-separated path relative to the component's
// own directory. It may not escape it — see [validExec].
Exec string `yaml:"exec" json:"exec"`
// Env are the environment variables the component wants passed through from
// fft's own environment, by name.
//
// It exists so a transport can keep reading the variable its ecosystem already
// defines — PUBSUB_EMULATOR_HOST is the standard way to point a Pub/Sub client at
// a local emulator, and a component should not have to learn an fft-specific
// spelling of it. Everything not named here and not built by [Environ] is still
// inherited; this list only covers the FFT_-prefixed names, which are stripped.
Env []string `yaml:"env,omitempty" json:"env,omitempty"`
// Targets are the subscription target types a transport component delivers, e.g.
// GOOGLE_CLOUD_PUB_SUB. Only meaningful for [KindTransport].
Targets []string `yaml:"targets,omitempty" json:"targets,omitempty"`
// Commands are the commands a command component adds. Only meaningful for
// [KindCommand].
Commands []Command `yaml:"commands,omitempty" json:"commands,omitempty"`
}
Manifest is a component.yaml: what a component is, and what it adds to fft.
It carries JSON tags as well as YAML ones because `fft component info -o json` renders it straight to the user.
func ParseManifest ¶
ParseManifest reads a component.yaml and checks it.
source names the file in error messages; it is the path the manifest was read from, or a description of where it came from.
type NotInstalledError ¶
type NotInstalledError struct {
// Name is the component's name.
Name string
// Command is the command path the user typed, for the message.
Command string
}
NotInstalledError is a component whose manifest fft has but whose executable it has not — either because it ships first-party and was never installed, or because an install was interrupted.
It exits exitcode.Config rather than 1: like a missing project, it is a configuration problem with a command that fixes it, and NotInstalledError.Hint is that command.
func (*NotInstalledError) Error ¶
func (e *NotInstalledError) Error() string
func (*NotInstalledError) ExitCode ¶
func (e *NotInstalledError) ExitCode() int
func (*NotInstalledError) Hint ¶
func (e *NotInstalledError) Hint() string
type Option ¶
type Option func(*Registry)
Option configures a Registry.
func WithFirstParty ¶
WithFirstParty replaces the compiled-in first-party table.
It exists for the specs, which need a first-party component to assert on without waiting for one to be shipped, and for `fft gen-docs`, which needs the table without the disk.
type Plan ¶
type Plan struct {
// Source is where it is coming from.
Source Source
// Manifest is what the archive says it is. It is read from the staged, verified
// archive, so by the time a Plan exists the download has happened and been
// checked; what has *not* happened is anything to the installed tree.
Manifest Manifest
// Dir is where it will be installed.
Dir string
// Replaces is the version already installed there, or "" for a fresh install.
Replaces string
// Digest is the SHA-256 of the archive, as verified. Empty for a --path install,
// which has no archive and no checksums file to check it against.
Digest string
// Signed reports that the release publishes a cosign signature over its
// checksums file. fft does not fetch or verify that signature — it only notes
// its presence, so the user knows one exists to check by hand. See
// [Plan.Verification], whose wording must not imply fft verified anything.
Signed bool
// contains filtered or unexported fields
}
Plan is what an install would do, worked out before anything is written.
It exists so the user can be shown the source, the version and the fact that a component runs as they do, and can say no — the confirmation is not a formality, it is the only point at which a human decides to trust this code.
func (Plan) Verification ¶
Verification is what the user should be told about the trust in this install, in one line.
type Problem ¶
type Problem struct {
// Dir is the directory, absolute.
Dir string
// Err is what is wrong with it.
Err error
}
Problem is a directory under the root that is not a component fft can use.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is the set of components this fft can see.
The zero value is usable and holds nothing, which is what a build with components disabled — or a machine with none installed — runs with. That matters: discovery happens while the command tree is being *built*, before any error could be reported to anyone, so a registry that cannot be opened must degrade to an empty one rather than fail.
func Open ¶
Open reads the component root.
A missing root is not an error — it is what a machine with no components looks like, and creating it just to find it empty would put a directory in the user's home for a feature they have not used. A manifest that cannot be read is recorded as a Problem and skipped; one bad component must not take the CLI down with it.
func (*Registry) All ¶
All is every component the registry knows, sorted by name — installed or not, first-party or not.
func (*Registry) Dir ¶
Dir is where a component of this name would be installed. It does not have to exist.
func (*Registry) Problems ¶
Problems are the directories under the root that are not usable components.
func (*Registry) Root ¶
Root is where this registry looks for components, and "" when they are disabled.
func (*Registry) Transports ¶
Transports are the installed transport components, which is what the emulator asks for when it works out where it can deliver an event.
type Scaffold ¶
type Scaffold struct {
// Name is the component's name, which is also its directory and — for a command
// component — the command it adds. It is checked as part of the manifest round-trip.
Name string
// Kind is what the component extends: [KindCommand] or [KindTransport].
Kind Kind
// Lang is the language the executable is written in: "shell", "go", "python" or
// "node". A "go" skeleton compiles to bin/<exec>; the others are interpreter
// scripts that run the moment they are installed.
Lang string
// Session is how much of the caller's session the command receives. Command kind
// only; ignored for a transport, which declares no commands.
Session Session
// FFTRequire pins github.com/Joessst-Dev/fft-cli in a Go transport's go.mod. Empty
// leaves it out, so the README's `go mod tidy` step resolves it — which is what a
// dev build of fft, whose version is no release tag, has to fall back to.
FFTRequire string
}
Scaffold describes a component to stamp out. Scaffold.Build renders the files — it does not write them; the caller decides where they land.
It is the single place the emitted manifest is kept correct: everything Build produces is built from a Manifest, marshalled, and round-tripped through ParseManifest before it is returned, so a scaffold that would not install cannot be produced in the first place.
type Session ¶
type Session string
Session is how much of the caller's tenant session a component command receives.
const ( // SessionNone hands over no credential at all. It is the right answer more often // than it looks: the emulator serves a fake tenant and has no use for a token. SessionNone Session = "none" // SessionRead hands over a short-lived id token and forces FFT_READ_ONLY, so the // component's own fft calls are gated the way the parent's would have been. SessionRead Session = "read" // SessionWrite hands over the same session without the forced read-only. SessionWrite Session = "write" )
type SessionInfo ¶
type SessionInfo struct {
BaseURL string
Email string
Token string
Tenant string
ProjectID string
Environment string
// ReadOnly is the parent's own read-only state, propagated so that a component
// granted a write session under a read-only project still refuses writes. The
// gate has already refused the command by then; this is the belt to its braces.
ReadOnly bool
}
SessionInfo is the tenant session the parent already resolved: everything a component needs to talk to the API, and nothing it needs to obtain it.
The token is short-lived and minted by the parent through its own token source, which is what keeps the keychain a thing only fft itself opens.
type Source ¶
type Source struct {
// Repo is an owner/repo on GitHub.
Repo string
// Version is the release tag, or "" for the latest release.
Version string
// Name is the component's expected name, when the spec implied one. It is
// checked against the manifest in the archive: an install of `emulator` that
// unpacks something called otherwise is a mismatch worth stopping on.
Name string
// Dir is a local directory to install from, which is how a component is
// developed and how a release archive is installed after being unpacked by hand.
// It is exclusive with Repo.
Dir string
}
Source is where a component is being installed from.
func ParseSource ¶
ParseSource reads an install spec.
Three forms, in the order a user is likely to type them:
emulator a component fft ships, from its own releases owner/repo somebody else's, latest release owner/repo@v1.2.3 somebody else's, pinned
type Streams ¶
Streams are the three the component inherits.
It inherits them rather than having them captured and relayed, which is what makes the output contract survive the process boundary: `fft something -o json | jq` puts the component's stdout in the pipe directly, with no buffering and nothing of fft's mixed into it.