> ## Documentation Index
> Fetch the complete documentation index at: https://sitectl.libops.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Component development

> How to define a sitectl component, what each field in the Definition struct means, and the workflow for adding a new one.

Components are defined in Go using the `component.Definition` struct from `pkg/component`. Plugins publish those definitions through the plugin SDK and register hidden component command handlers for typed RPC dispatch. Core uses the definitions to plan create and render component metadata, then calls the handlers to inspect, mutate, and reconcile project state.

## The Definition struct

```go theme={null}
type Definition struct {
    Name                string
    DefaultState        State
    DefaultDisposition  Disposition
    AllowedDispositions []Disposition
    Guidance            StateGuidance
    PromptOnCreate      bool
    FollowUps           []FollowUpSpec
    Gates               GateSpec
    Dependencies        Dependencies
    Behavior            Behavior
    On                  DomainSpec
    Off                 DomainSpec
}
```

### Core identity

`Name` is the machine-readable identifier used in commands like `sitectl set fcrepo off`. It must be unique within the plugin.

`DefaultState` is `on` or `off`, which controls what the component does if the operator never sets it explicitly. Most components default to `on` to match the upstream template.

### Create flow

`PromptOnCreate` controls whether `sitectl create isle` asks the operator about this component. Set it to `true` for components that represent a meaningful architectural choice that should be made at install time, such as whether to include fcrepo or Blazegraph.

`FollowUps` is a list of additional non-secret questions that appear when a specific state or disposition is chosen. For example, fcrepo asks which file system URI to use when it is enabled. Follow-up specs include:

| Field                      | Meaning                                                            |
| -------------------------- | ------------------------------------------------------------------ |
| `Name`                     | Machine-readable identifier                                        |
| `Label`                    | Short human-readable name used in review output                    |
| `FlagName`                 | CLI flag that pre-answers this question (e.g. `--fcrepo-fs-type`)  |
| `FlagUsage`                | Help text for the generated flag                                   |
| `Question`                 | Text shown to the operator in interactive mode                     |
| `Choices`                  | Valid answers, if the question is a select                         |
| `DefaultValue`             | Pre-selected answer                                                |
| `BoolValue` / `MultiValue` | Whether the generated flag is a boolean or repeatable string value |
| `Required`                 | Whether the selected disposition requires a value                  |
| `PromptOnCreate`           | Whether to ask this follow-up during create                        |
| `AppliesTo`                | Which state this follow-up applies to (`on` or `off`)              |
| `AppliesToDisposition`     | More precise disposition filter when `on`/`off` is not enough      |
| `CustomPrompt`             | Additional operator guidance rendered before input                 |

Follow-up flags are forwarded to the plugin as passthrough argv so Cobra can parse
the plugin-owned flags. They can be visible in process listings, and the typed RPC
params sensitivity check does not cover passthrough `Args`. Never use a follow-up for
passwords, tokens, private keys, secret keys, or any other sensitive value. Secret
material must use a purpose-built stdin, file, environment-reference, or secret-store
flow whose transport and redaction contract is explicit.

### Domains: On and Off

`On` and `Off` are both `DomainSpec` values that describe what changes when the component is in each state:

```go theme={null}
type DomainSpec struct {
    Compose YAMLStateSpec
    Drupal  YAMLStateSpec
    Files   FileStateSpec
}
```

`Compose` rules describe changes to Compose YAML. `Drupal` rules describe changes to the Drupal config sync directory. Each `YAMLStateSpec` holds a list of `YAMLRule` entries:

```go theme={null}
type YAMLRule struct {
    Files       []string   // files to target
    Exclude     []string   // matching files to skip
    SourceFiles []string   // canonical source candidates for restore
    Path        string     // YAML path within the file (dot-separated)
    Op          RuleOp     // set, delete, restore, replace, contains, or not_contains
    Value       any        // value to set (for OpSet)
    Old         any        // value to replace (for OpReplace)
}
```

Operation support is domain-specific. Compose mutation accepts `set`, `restore`,
and `delete`; additional operations such as `replace`, `contains`, and
`not_contains` are used by supported state-detection paths. Do not assume every
operation is writable in every domain—cover both detection and application in the
component tests.

`Files` is the generic project-file domain for component-owned state outside
Compose and Drupal YAML. It uses `FileStateSpec` and `FileRule`:

```go theme={null}
type FileRule struct {
    Files       []string
    Op          RuleOp
    Path        string
    Value       any
    StartMarker string
    EndMarker   string
    Content     string
}
```

When `Path` is set, the target is JSON and the rule sets, restores, or deletes the
named object path while retaining unrelated fields. Without `Path`, a text rule owns
only its marked block; `set`/`restore` writes the block and `delete` removes it. Use
stable, component-specific markers and the smallest possible JSON path or text block.
The same context file-access layer supports local and remote projects, so a generic
file rule must be deterministic, bounded, and idempotent in both environments.

Generic file rules do not make an entire file component-owned. The normal field
ownership contract still applies: unmarked text, adjacent JSON fields, and all other
downstream content must survive set, disable, and converge operations.

For operational work that structured YAML and generic file rules cannot express, `ComponentSpec` supports lifecycle hooks (`BeforeEnable`, `AfterEnable`, `BeforeDisable`, `AfterDisable`) that run Go functions with access to the Docker client and context config. Hooks are an exception for runtime transitions, not a reason to hide deterministic file ownership in arbitrary code.

### Field ownership contract

Treat every component as the owner of a small, explicit part of the downstream project. Record the contract next to the definition and enforce it in tests:

* List every project-relative file and smallest semantic YAML unit the component may change for each allowed disposition: for example, a scalar key, identified mapping or sequence entry, or generated block. Components may share a containing map or list when they own disjoint units and preserve one another's entries. They must not own the same unit.
* Preserve unknown keys, adjacent fields, unrelated sequence entries, and downstream policy outside those units. Enabling, disabling, or repairing drift may change owned fields, but must not normalize or reconstruct neighboring configuration.
* Replace an entire file only when the component is its exclusive owner and the contract says so. An explicit base-owner/nested-augmenter relationship is the exception: document the parent and child units, make the convergence pipeline reconcile the base owner's selected disposition before every augmenter and reapply each augmenter's selected disposition after any base repair, and state that downstream edits inside the base-owned file are not guaranteed to survive base repair. Do not depend on incidental registration, lexical, or rule order.
* Test every allowed disposition, an enable-disable-enable cycle, repeated application for idempotence, deliberate drift followed by repair, and sentinel unrelated fields that must survive every transition. For disjoint units in a shared container, apply the components in both orders and prove that their writes commute. For a declared base-owner/nested-augmenter relationship, cover every enabled/disabled combination in the documented order and reconverge after repairing either owner.

For example, the ISLE `fcrepo` and `blazegraph` components share three Drupal
context files but own different named entries under each reaction's `actions`
mapping. Their apply path seeds a missing context once, then reconciles only
those exact entries. It tests every Fedora/Blazegraph state transition while
sentinel keys and existing action bodies prove that downstream configuration
survives.

### Gates

`Gates` controls safety checks before a state change is applied:

```go theme={null}
type GateSpec struct {
    LocalOnly           bool
    DisableConfirmation string
    EnableConfirmation  string
}
```

Set `LocalOnly: true` for components that should only be changed on local contexts. Set `DisableConfirmation` or `EnableConfirmation` to a custom prompt string. If empty, sitectl uses a default that mentions the possibility of rewriting Compose files and Drupal config.

### Behavior

`Behavior` records operational metadata that helps sitectl (and operators) understand what a state change entails:

```go theme={null}
type Behavior struct {
    Idempotent bool
    Enable     TransitionBehavior
    Disable    TransitionBehavior
}

type TransitionBehavior struct {
    DataMigration DataMigrationRequirement  // none, backfill, or hard
    Summary       string
}
```

`DataMigration` tells operators whether enabling or disabling this component requires a data migration:

| Value      | Meaning                                                                                              |
| ---------- | ---------------------------------------------------------------------------------------------------- |
| `none`     | No data migration needed                                                                             |
| `backfill` | Existing data needs to be backfilled after the change, but the change itself is safe to apply        |
| `hard`     | A data migration must happen before applying the change; applying without migrating may corrupt data |

### Dependencies

`Dependencies.DrupalModules` lists which Drupal modules must be present when the component is enabled, and how sitectl should treat them if the component is later disabled:

```go theme={null}
type DrupalModuleDependency struct {
    Module          string
    ComposerPackage string
    Mode            DrupalModuleDependencyMode  // strict or enable_only
}
```

`strict` means the module is part of the component's contract: enable it when the component is enabled, and consider it out of place when the component is disabled.

`enable_only` means the module must exist when the component is enabled, but disabling the component does not imply removing or uninstalling it.

## Registering a component

Plugins publish component metadata and register its apply handlers at startup:

```go theme={null}
definitions := orderedComponentDefinitions()
sdk.RegisterComponentDefinitions(definitions...)
sdk.RegisterComponentCommand(componentExtensionCmd)
```

`RegisterComponentDefinitions` exposes the v1 `Definition` metadata used by create and core component review. `RegisterComponentCommand` installs the plugin's hidden `list`, `describe`, `reconcile`, and `set` handlers; their flags must match the typed RPC parameter contract. Shared Compose service components should use `sdk.RegisterServiceComponents(...)`, which registers both metadata and standard handlers.

The older `component.Registry` stores implementations of the legacy `component.Component` interface. It does not accept `component.Definition` values and is not how a v1 plugin publishes component metadata.

## When to define a new component

A feature is a good candidate for a component if:

* Operators frequently ask how to enable or disable it (it comes up in support channels)
* Enabling or disabling it requires coordinated changes across Compose files and Drupal config
* It represents a meaningful architectural choice that should be made at install time

If a feature is always on and has no meaningful off state, it does not need to be a component.

If the component represents a reusable, self-contained service such as Traefik, Solr, MariaDB, Valkey, or Memcached, follow the [Service components](/contributing/service-components) architecture. The shared operation belongs in core `sitectl`; the application plugin should only add app-specific wiring.

## Cross-domain feature bundles

Use an application-plugin feature bundle when one operator-visible capability must
change several application domains as a unit—for example, a Compose service plus a
Drupal action, or Composer requirements plus Drupal and search configuration. Do not
model only the container when that would leave the application half-configured, and
do not move stack-specific rules into core merely because the bundle touches
Compose.

A bundle should declare whole-owned canonical assets separately from narrow
mutations, preflight every target input before its first write, and expose the same
state through create, `set`, describe, validate, and converge. Keep it local-only
when the result must be reviewed and committed in the downstream fork. Dependency
manifests can be owned narrowly, but package-manager lock files, live configuration
imports, deployment, and data backfills remain explicit downstream operations.

The ISLE [`mergepdf` and `hocr-search` bundles](/plugins/isle/feature-bundles) are the
reference implementation and publish their exact whole-file, field, compatibility,
and rollout boundaries for operators.

## Component workflow

<Steps>
  <Step title="Define the component">
    Write a new Go file in the plugin's `pkg/components` directory. Implement the `Definition` struct with `On` and `Off` domain specs. Add structured YAML or generic file rules for the observable state that `describe`, `set`, and `converge` should detect.
  </Step>

  <Step title="Register it">
    Add the new definition to the plugin's ordered definition set. Ensure the SDK registers that set and the plugin's hidden component command tree exactly once.
  </Step>

  <Step title="Add it to the create flow">
    If the component requires a decision at install time, set `PromptOnCreate: true`, add any `FollowUps` needed, and thread the decision through the plugin's create request/options object. The create path should apply the same behavior as top-level `set` so a new checkout and an existing checkout stay consistent.
  </Step>

  <Step title="Add the apply path">
    Use the repo's existing component apply pattern. For simple components, structured YAML mutation helpers are usually enough. For larger generated config blocks, put the static config in `pkg/create/assets/...` and load it with Go `embed` instead of keeping inline YAML strings in Go code.
  </Step>

  <Step title="Write tests">
    Add focused tests for the component definition, create option resolution, the set/apply path, and any generated files. If the component is intentionally disabled during integration tests, make that explicit with a create flag instead of relying on an implicit default.
  </Step>

  <Step title="Document it">
    Add a page in `plugins/isle/` (or the relevant plugin section), add it to `docs.json`, and link to it from the plugin overview. Document when to enable or disable the component, create-time flags, follow-up environment variables, generated files, and any production caveats.
  </Step>
</Steps>

## Component Checklist

Before opening a component PR, check that it covers the full operator workflow:

* `Definition` includes clear guidance, allowed dispositions, behavior summaries, data migration impact, and `LocalOnly` when changes should only happen on local checkouts.
* `PromptOnCreate` is true for features operators should decide on during initial site creation.
* Create flags and recreate-command output include the component so automated installs and setup commits are reproducible.
* `set`, create, and converge/describe detection agree on what `on`, `off`, and `drifted` mean.
* Every follow-up value is explicitly non-secret and safe to expose through process argv.
* Generic file rules own the smallest JSON path or marked text block and preserve sentinel downstream content around it.
* Static generated YAML or templates live under `pkg/create/assets/...` and are loaded with Go `embed`; avoid large inline YAML blocks in Go.
* Runtime warnings are printed for insecure defaults, required credentials, destructive changes, or behavior that can surprise production operators.
* Integration tests explicitly set the component state, even when the intended state is the default.
* Plugin documentation explains the operational reason for any unusual design choice, such as vendoring or mounting local plugin source to avoid production startup dependencies on third-party services.
