> ## 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.

# Overview

> Contributor guidance for sitectl, including local development setup, UI architecture rules, and release publishing.

export const TUI = () => <Tooltip headline="TUI" tip="Terminal User Interface: an interactive command-line interface for navigating and operating sitectl.">
    TUI
  </Tooltip>;

# Contributing

## Local Plugin Development

`sitectl` has a core binary and optional plugin binaries named `sitectl-<plugin>`.

The current local development plugin chain in this workspace is:

* `sitectl`: core binary; owns the operator-facing commands
* `sitectl-isle`: ISLE plugin; stack logic, component definitions, ISLE-specific jobs
* `sitectl-drupal`: Drupal plugin; drush, sync, ULI; included by the ISLE plugin

Core sitectl owns the operator-facing command shape. Plugins extend that behavior through the private `__sitectl-rpc` protocol dispatched based on the active context's plugin field.

The SDK registers that private entrypoint automatically. Plugin code should register SDK handlers and runners, not define its own transport commands.

Core commands that fan out to plugins:

* `sitectl debug` → `debug.run`
* `sitectl validate` → core validators + `validate.run` (results merged)
* `sitectl healthcheck` → core Compose runtime checks + `healthcheck.run` (results merged)
* `sitectl verify` → `verify.run` (results rendered by core)
* `sitectl converge` → `converge.run`
* `sitectl set` → `set.run`
* `sitectl deploy` → `deploy.run`
* `sitectl job run` → `job.run`
* `sitectl component describe` → `component.describe`
* `sitectl component set` → `component.set`

When a context belongs to `isle`, core sitectl routes dispatch commands to `sitectl-isle`. The ISLE plugin can further invoke included plugins such as `sitectl-drupal`.

See [Plugin hierarchy](/contributing/plugin-hierarchy) for the full protocol.

## Local Install Workflow

For local development, install the binaries into a directory on your `$PATH` so the core binary can discover and invoke plugin binaries. From the core repo:

```bash theme={null}
make install
```

That target will:

1. Build and install `sitectl`
2. Change into `../sitectl-isle` and run `make install`
3. Change into `../sitectl-drupal` and run `make install`

The plugin `install` targets run `make work` before building so they use the local core `sitectl` checkout during development.

If you are only working on a single plugin:

```bash theme={null}
cd ../sitectl-isle && make install
cd ../sitectl-drupal && make install
```

## Why `make install` matters

If you only rebuild `sitectl` locally but do not install the plugin binaries into a directory on `PATH`, core command dispatch will not see the current local plugin builds. Installing the full chain keeps the stack aligned while you work on:

* core command routing in `sitectl`
* stack logic in `sitectl-isle`
* Drupal-specific extensions in `sitectl-drupal`

## UI Architecture

`sitectl` supports two interaction modes:

* one-off command execution such as `sitectl compose ps`
* an embedded <TUI /> dashboard launched by running `sitectl` with no additional arguments

Because both modes need to share behavior, interactive command UIs must be designed as composable Bubble Tea models instead of bespoke terminal flows.

## Rule

When a command needs interactive UI:

* keep business logic separate from UI state and rendering
* make the UI self-contained inside the command or shared UI package
* ensure the same UI can run standalone or be embedded inside the dashboard

In practice, command implementations should follow this split:

* **service layer:** pure command logic and side effects
* **UI layer:** Bubble Tea model and Bubbles-based components
* **Cobra layer:** chooses between non-interactive execution and launching the UI

## Required Libraries

Interactive `sitectl` UIs should build on the shared <TUI /> stack already in use:

* `bubbletea` for state, events, and screen management
* `bubbles` for list, help, input, viewport, progress, and similar primitives
* `lipgloss` for styling and layout
* `bubblezone` for click targets and mouse hit detection where needed
* `harmonica` for motion and transitions where appropriate
* `ntcharts` for terminal charts where appropriate

## What Not To Do

Do not implement custom terminal widgets when the library stack already provides them:

* do not hand-roll a select menu when `bubbles/list` fits
* do not hand-roll a text input when `bubbles/textinput` or `textarea` fits
* do not hand-roll help footers when `bubbles/help` fits
* do not hand-roll scroll containers when `bubbles/viewport` fits

`lipgloss` is for presentation and composition, not a replacement for Bubble Tea or Bubbles interaction primitives.

## Shared Components

Reusable interaction primitives should live in shared UI packages so commands and the dashboard can both consume them:

* shared prompt, select, and input components belong in `pkg/ui`
* command-specific interactive screens can live near the command, but should still be Bubble Tea models
* older bespoke prompt implementations should be migrated to shared Bubble Tea and Bubbles components over time

## Design Goal

A command that has an interactive flow should be embeddable in the dashboard without rewriting its UI logic. That means a command UI should be structured so it can be:

* launched directly from Cobra
* pushed or mounted inside the dashboard <TUI />

If a proposed command UI cannot be reused that way, it should be redesigned before being added.

## Release Publishing

GoReleaser builds the release artifacts, including Linux packages via `nfpms`.

* GitHub release publishing: [goreleaser.yaml](https://github.com/libops/sitectl/blob/main/.github/workflows/goreleaser.yaml)

Release publishing uses two independent credentials:

* The GoReleaser job uses only the caller's source-repository-scoped `GITHUB_TOKEN` to create the GitHub release and upload its artifacts.
* A separate, isolated tap-reconciliation job may use `HOMEBREW_REPO` to update the `libops/homebrew` repository. That cross-repository token is not exposed to GoReleaser or substituted for `GITHUB_TOKEN`.

The application plugin template leaves package-repository publication disabled with `publish-package-repo: false`. A derived plugin should enable it only after its repository has a trusted package publisher and the required cloud identity and variables. Leaving it disabled still publishes GitHub release artifacts and the native package files attached to the release. Homebrew publication is independent and runs only when the derived repository points its isolated tap-reconciliation job at a tap it controls and provides `HOMEBREW_REPO` to that job.

### Linux package publishing

This repo publishes Debian and RPM repositories through the shared libops packaging infrastructure.

GitHub Actions workflows:

* core caller workflow: [sitectl `goreleaser.yaml` at the documented `v1.0.0` commit](https://github.com/libops/sitectl/blob/65cfde137a58ba14aaa9a1512d88b943888872f3/.github/workflows/goreleaser.yaml)
* core reusable workflow: [`reusable-goreleaser.yaml` at `481df51116aed2efd1c002ef1ef6a287699828a0`](https://github.com/libops/terraform-linux-packages/blob/481df51116aed2efd1c002ef1ef6a287699828a0/.github/workflows/reusable-goreleaser.yaml)
* package publisher pinned by that core workflow: [`terraform-linux-packages` at `72d2f0c3b01e5e396d6db074108b95e95eedf4d4`](https://github.com/libops/terraform-linux-packages/tree/72d2f0c3b01e5e396d6db074108b95e95eedf4d4)
* ArchivesSpace, OJS, Omeka Classic, Omeka S, and WordPress v1.0.0 reusable workflow: [`sitectl-plugin-goreleaser.yaml` at `39f81174b7b899f66aee31035308e1d9b1f3e165`](https://github.com/libops/.github/blob/39f81174b7b899f66aee31035308e1d9b1f3e165/.github/workflows/sitectl-plugin-goreleaser.yaml)
* Drupal v1.0.0 and LibOps v1.3.0 reusable workflow: [`sitectl-plugin-goreleaser.yaml` at `e1e30b58c9c566f72b22f03e637cd5218d635727`](https://github.com/libops/.github/blob/e1e30b58c9c566f72b22f03e637cd5218d635727/.github/workflows/sitectl-plugin-goreleaser.yaml)
* package publisher pinned by the five-application and Drupal/LibOps workflow revisions: [`terraform-linux-packages` at `285183831e6e5c14ec34d87e208629949f5b627c`](https://github.com/libops/terraform-linux-packages/tree/285183831e6e5c14ec34d87e208629949f5b627c)
* ISLE v1.0.0 release-assets and Homebrew recovery workflow: [`sitectl-plugin-goreleaser.yaml` at `8e27d95846671a9e319f1900e86a488a1d4f39b3`](https://github.com/libops/.github/blob/8e27d95846671a9e319f1900e86a488a1d4f39b3/.github/workflows/sitectl-plugin-goreleaser.yaml)
* ISLE v1.0.0 final Linux-package recovery workflow: [`sitectl-plugin-goreleaser.yaml` at `77724fe807ede3e0808d4556f47e4ad0ae266bac`](https://github.com/libops/.github/blob/77724fe807ede3e0808d4556f47e4ad0ae266bac/.github/workflows/sitectl-plugin-goreleaser.yaml)
* package publisher pinned by that final ISLE recovery workflow: [`terraform-linux-packages` at `b14fd2f95b4017e897c595b3321b9cc3f48b5ddd`](https://github.com/libops/terraform-linux-packages/tree/b14fd2f95b4017e897c595b3321b9cc3f48b5ddd)

The core and application-plugin paths are intentionally listed separately. The
published plugin set spans immutable revisions of the shared workflow. ISLE's
recovery used the `8e27d95` revision to restore release assets and reconcile
Homebrew, then the `77724fe` revision and its `b14fd2f` package publisher to
complete Debian and RPM repository publication. The [successful final recovery
run](https://github.com/libops/sitectl-isle/actions/runs/29549667637) records the
completed package publication. Advance a full SHA only with the caller and
publisher contract tests in the same change; never replace it with `main`.

Required GitHub secrets:

* `HOMEBREW_REPO`: cross-repository token available only to the isolated Homebrew tap-reconciliation job

Required GitHub variables:

* `LIBOPS_PACKAGES_GCLOUD_OIDC_POOL`: Workload Identity provider resource name
* `LIBOPS_PACKAGES_GCLOUD_PROJECT`: Google Cloud project ID that holds the package infrastructure
* `LIBOPS_PACKAGES_GSA`: Google service account email used by GitHub Actions
* `LIBOPS_PACKAGES_GCS_BUCKET`: bucket name that hosts the published package repository
* `LIBOPS_PACKAGES_APTLY_GPG_KEY_ID`: GPG key ID or fingerprint to use for signing
* `LIBOPS_PACKAGES_APTLY_GPG_PRIVATE_KEY_SECRET`: Secret Manager secret ID that stores the armored private key
* `LIBOPS_PACKAGES_APTLY_GPG_PASSPHRASE_SECRET`: Secret Manager secret ID that stores the signing key passphrase

Optional GitHub variables:

* `GCS_BUCKET_PREFIX` default: empty
* `APTLY_DISTRIBUTIONS` default: `bookworm`
* `APTLY_COMPONENT` default: `main`
* `APTLY_ARCHITECTURES` default: `amd64,arm64`
* `APTLY_PUBLISH_PREFIX` default: `.`
* `APTLY_ORIGIN` default: `libops`
* `APTLY_LABEL` default: `sitectl`
* `APTLY_PUBLIC_KEY_NAME` default: `sitectl-archive-keyring`
* `RPM_REPOSITORY_PATH` default: `rpm`

The publisher signs the APT Release metadata. For RPM, it signs `repodata/repomd.xml`; the repository metadata authenticates package checksums, but the individual RPM payloads are not separately signed. Client configuration therefore uses `repo_gpgcheck=1` and `gpgcheck=0`. Do not advertise per-RPM signatures or enable `gpgcheck=1` until package signing is implemented.

The workflow rebuilds Debian and RPM repository metadata from the current release artifacts only. That is sufficient for fresh installs and upgrades, but does not preserve older package versions for pinning or rollback. A version in cloud-compose or another package manifest is durable only while the artifact remains available. Production operators that need rollback must retain a repository snapshot or archive the matching GitHub release package assets and `checksums.txt` in controlled storage.
