cli

package
v0.6.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 11, 2026 License: MIT Imports: 38 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrAgentApplyFailed = errors.New("agent apply failed")

ErrAgentApplyFailed is returned after the apply summary is written when one or more approved actions failed to apply. The loop keeps going past individual failures, but the process must still exit non-zero — scripted runs check $?, not the "errors: N" stdout line.

View Source
var ErrCheckFailed = errors.New("repository check failed")

ErrCheckFailed is returned after a check report is written when the repository has integrity or operational health failures.

View Source
var ErrDoctorFailed = errors.New("sentra doctor failed")

ErrDoctorFailed is returned after a doctor report is written when one or more required checks failed.

Functions

func ConfigureSlog

func ConfigureSlog(flags *RootFlags, tuiMode bool) (cleanup func() error, err error)

ConfigureSlog builds and installs a slog.Default logger from the parsed root flags. Returns a cleanup func that closes any opened log file (no-op when LogFile is empty). main.go calls this once at startup; subcommand bodies use slog.Info/Warn/Error directly through slog.Default().

Behavior:

  • LogLevel: parsed case-insensitively; unknown levels fall back to warn rather than panic, so a typo doesn't crash the CLI.
  • LogFormat: text or json; anything else falls back to text.
  • LogFile: opened append-only with 0600. Errors are returned; callers should fall back to stderr or fail the command.
  • tuiMode: when true and LogFile is empty, logs go to io.Discard so the TUI's alt-screen stays clean. Setting --log-file overrides this — operators running the TUI on cron can still capture diagnostics by writing to a file.

func DefaultAWSCheckSDKIdentity

func DefaultAWSCheckSDKIdentity(ctx context.Context, cfg *config.Config) error

DefaultAWSCheckSDKIdentity verifies credentials through the AWS SDK credential chain. Thin wrapper over diag.CheckSDKIdentity so the doctor's nil-fallback and setup's identity checker keep their names.

func DefaultUIRunner

func DefaultUIRunner(app tui.App) error

DefaultUIRunner is the production launcher: wraps the App in a tea.Program with alt-screen and runs it. Wiring this up here keeps the cmd/sentra main.go terse.

Refuses to launch when stdout isn't a TTY: alt-screen escape codes would otherwise be written into a pipe, file, or CI log, polluting the consumer's stdout with no useful output. Scripts piping `sentra` should call a JSON-emitting subcommand (`snapshots --json`, `agent scan --json`) instead.

The message names both non-interactive routes because `sentra setup` reaches this same refusal — setup is a launcher for this TUI. Telling someone who typed `setup` to run `sentra ui` restates the thing that just refused; `sentra init` is the flow that configures a repository without a terminal.

func HuhAgentConfirm

func HuhAgentConfirm(prompt string) (bool, error)

HuhAgentConfirm is the production Confirm callback for the agent's per-recommendation prompt flow. Negative is "No, skip" — declining one recommendation moves to the next, it does not abort the run.

func HuhBackupApplyConfirm

func HuhBackupApplyConfirm(prompt string) (bool, error)

HuhBackupApplyConfirm is the production Confirm implementation for `sentra backup apply`. Affirmative reads as the action ("Yes, snapshot") so the operator confirms what they're authorizing rather than a generic "Yes."

func HuhConfirm

func HuhConfirm(prompt string) (bool, error)

HuhConfirm is the production Confirm implementation for `sentra prune`. Wired up by main.go; tests inject their own callback to keep the run deterministic.

Affirmative is "Yes, delete" and the negative is "No, abort" — wording designed so the choice reads unambiguously when the cursor lands on the default ("No, abort" is the safer pick).

func NewAgent

func NewAgent(deps AgentDeps) *cobra.Command

NewAgent returns the cobra command for the `agent` parent. It owns no business logic itself; only registers `scan` as a subcommand. Future agent subcommands (e.g. `agent eval`) belong here.

func NewAgentAdviseIgnore

func NewAgentAdviseIgnore(deps AgentDeps) *cobra.Command

NewAgentAdviseIgnore returns `sentra agent advise-ignore [root]`.

func NewAgentScan

func NewAgentScan(deps AgentDeps) *cobra.Command

NewAgentScan returns the cobra command for `sentra agent scan`. Without --apply the command is dry-run: it prints a styled table of recommendations and exits. With --apply, each recommendation goes through the per-action handler map after a Confirm prompt (skippable with --yes).

Flags:

  • --apply actually execute approved recommendations
  • --json emit recommendations as JSON instead of a table
  • --yes skip the confirm prompt under --apply
  • --root <path> filesystem root to scan (default ".")
  • --local-only convert heuristic findings without calling the LLM
  • --no-llm alias for --local-only
  • --categories a,b only triage matching finding categories/heuristics
  • --config <path> sentra.yaml path (default ./sentra.yaml)
  • --max-tool-calls N override the orchestrator's tool-call budget

func NewBackup

func NewBackup(deps BackupDeps) *cobra.Command

NewBackup returns the cobra command for `sentra backup <path>`. Flags:

  • --tag string human-readable label persisted on the snapshot
  • --config path overrides the default sentra.yaml location

The command flow is:

  1. Load sentra.yaml (env overlays applied)
  2. Resolve passphrase (deps.Passphrase callback)
  3. Open the repo via deps.NewStore + repo.Open
  4. CreateSnapshot with a ui.ByteProgress reporter, repainted to stderr every progressTickInterval until the call returns
  5. Print the final summary (snapshot ID, files, bytes, new bytes)

func NewCheck

func NewCheck(deps CheckDeps) *cobra.Command

NewCheck returns the cobra command for `sentra check`.

func NewDiff

func NewDiff(deps DiffDeps) *cobra.Command

NewDiff returns the cobra command for `sentra diff <snap-a> <snap-b>`. Default rendering is a styled table with columns "Status", "Path"; --json emits a stable {added, removed, changed} schema.

func NewDoctor

func NewDoctor(deps DoctorDeps) *cobra.Command

NewDoctor returns the read-only setup and repository diagnostic command.

func NewInit

func NewInit(deps InitDeps) *cobra.Command

NewInit returns the cobra command for `sentra init`. The command validates the destination, opens the configured store via deps, calls repo.Init, and writes sentra.yaml. --force allows clobbering an existing sentra.yaml; that's required when the user has changed passphrase / bucket / region after the fact and is fine with the store being re-bootstrapped (typically against a fresh prefix).

Flags:

  • --force overwrite an existing sentra.yaml + re-bootstrap repo
  • --bucket S3 bucket (required if no sentra.yaml exists yet)
  • --region AWS region; e.g. us-west-2
  • --profile AWS shared-config profile
  • --endpoint-url MinIO/LocalStack base URL; empty for AWS
  • --prefix optional key prefix inside the bucket

The flags layer on top of any existing sentra.yaml + env overlays so `sentra init --force --bucket new` preserves region/profile from the previous file unless those flags are also passed.

func NewLocal

func NewLocal(deps LocalDeps) *cobra.Command

NewLocal returns the cobra command for `sentra local`, a dev-convenience flow that boots a local MinIO, points Sentra at it, and opens the TUI with the first-run wizard pre-filled for MinIO.

It never touches the real sentra.yaml: it launches against .sentra-local.yaml, seeds the wizard with the local MinIO S3 coordinates (endpoint http://localhost:9000, bucket sentra-test, region us-east-1), and exports the well-known minioadmin credentials — but only when the user has not already set AWS credentials, so a real environment is never clobbered.

func NewLs

func NewLs(deps LsDeps) *cobra.Command

NewLs returns the cobra command for `sentra ls <snapshot>` — the CLI answer to "is my file in this snapshot?". The TUI has detail views for this; scripts and terminals had nothing.

func NewMCP added in v0.2.0

func NewMCP(deps MCPDeps) *cobra.Command

NewMCP returns the cobra command for `sentra mcp`: a Model Context Protocol server over stdio, exposing metadata-only snapshot queries and two-phase (plan → token → confirm) backup/restore to MCP clients.

stdin/stdout ARE the protocol channel, so the passphrase resolves non-interactively only (env / --passphrase-file / keyring) — a missing source is a startup error, never a prompt. Logs stay on stderr, which stdio MCP leaves free.

func NewPasswd

func NewPasswd(deps PasswdDeps) *cobra.Command

NewPasswd returns the cobra command for `sentra password`. The command flow:

  1. Load sentra.yaml (config.Load).
  2. Open the blobstore via deps.NewStore.
  3. Read the OLD passphrase via deps.Passphrase.
  4. repo.Open under the old passphrase. If this fails (wrong passphrase or tampered config), the new-passphrase callback is NEVER invoked — operators get a clean "wrong passphrase" without being prompted for the new one (which would imply they authenticated successfully).
  5. Read the NEW passphrase via deps.NewPassphrase (file flag or interactive confirm-on-entry prompt).
  6. Validate: non-empty, length >= minPasswdNewPassphraseLen, not equal to the old passphrase.
  7. Call repo.Repo.Passwd which acquires the advisory lock, rotates salt + wrap + MAC, and writes the new config blob.
  8. Print a one-line summary.

Refusal cases short-circuit before any S3 write.

func NewPin

func NewPin(deps PinDeps) *cobra.Command

NewPin returns the cobra command for `sentra pin <snapshot>`: mark a snapshot as protected so retention never drops it and deletion refuses it until unpinned.

func NewPolicy

func NewPolicy(deps PolicyDeps) *cobra.Command

NewPolicy returns the command group for named backup policies.

func NewPrune

func NewPrune(deps PruneDeps) *cobra.Command

NewPrune returns the cobra command for `sentra prune`. Without --apply the command is dry-run: it prints what would be deleted but does not modify the store. With --apply, the user is prompted for confirmation (huh.NewConfirm in production; PruneDeps.Confirm in tests) unless --yes is passed for scripting.

The retention policy is the union of (config defaults < flag overrides). Each --keep-* flag layers on top of the corresponding retention key from sentra.yaml.

Flags:

  • --keep-last N override retention.keep_last
  • --keep-daily N override retention.keep_daily
  • --keep-weekly N override retention.keep_weekly
  • --keep-monthly N override retention.keep_monthly
  • --apply actually delete (default: dry-run)
  • --yes skip the confirm prompt (apply-mode only)
  • --explain print why each snapshot is kept or dropped
  • --config <path> sentra.yaml path (defaults to ./sentra.yaml)

func NewRecoveryKit

func NewRecoveryKit(deps RecoveryKitDeps) *cobra.Command

NewRecoveryKit returns `sentra recovery-kit`.

func NewRestore

func NewRestore(deps RestoreDeps) *cobra.Command

NewRestore returns the cobra command for `sentra restore <snap-id> <dest-dir>`. The dest-dir must either not exist (then it's created) or be empty; a non-empty dest is rejected by repo.Restore to prevent silent merging on top of stale content.

Flags:

  • --config override the default sentra.yaml location

Progress: a ui.ByteProgress reporter is wired into RestoreOptions and a goroutine repaints the bar to stderr on the same cadence as the backup command.

func NewRoot

func NewRoot(version, commit, date string) *cobra.Command

NewRoot returns the root cobra command without exposing the flags struct. Useful for tests / callers that don't care about reading persistent flag values back. The flags are still registered (and parsed normally), they're just not surfaced to the caller.

func NewRootWithFlags

func NewRootWithFlags(version, commit, date string, flags *RootFlags) *cobra.Command

NewRootWithFlags is the wiring point for production: pass in a pointer to a RootFlags and the persistent flag values get written into it as cobra parses argv. Subcommand bodies capture the same pointer via their Deps and read the live values at run time.

Persistent flags:

  • --passphrase-file <path> sourced first by the passphrase resolver

func NewSchedule

func NewSchedule(deps ScheduleDeps) *cobra.Command

NewSchedule returns the command group for installing policy schedules.

func NewSetup

func NewSetup(deps UIDeps) *cobra.Command

NewSetup returns the cobra command for `sentra setup`. It is a thin launcher for the TUI setup wizard: the wizard drives setup.Engine directly, so a second huh-based wizard here would be a duplicate of the same flow against the same engine.

The command forces the wizard even when sentra.yaml already exists, which makes reconfiguring a normal supported flow. The wizard's review stage is the confirmation gate for the overwrite, so there is no --force flag.

func NewSnapshots

func NewSnapshots(deps SnapshotsDeps) *cobra.Command

NewSnapshots returns the cobra command for `sentra snapshots`. Flags:

  • --json emit a JSON array instead of the styled table
  • --config override the default sentra.yaml location

The default output is a lipgloss table with columns `ID, Created, Tag, Files, Bytes` rendered via ui.RenderTable. The JSON output emits a stable schema: each row has id, created_at (RFC3339), tag, files (int), bytes (int), new_bytes (int).

func NewStats

func NewStats(deps StatsDeps) *cobra.Command

NewStats returns the cobra command for `sentra stats`: dedup ratio, logical-vs-stored bytes, and each snapshot's unique footprint — the "what is this repo costing me and which snapshot owns it" view.

func NewSync

func NewSync(deps SyncDeps) *cobra.Command

NewSync returns the cobra command for `sentra sync`. The command flow:

  1. Validate --dst-config is set (refuse with a clear error).
  2. Load the source's sentra.yaml via config discovery (cwd, else the user-level config).
  3. Load the destination's sentra.yaml from --dst-config.
  4. Refuse if source and destination resolve to the same bucket + prefix (a no-op at best, deadlock at worst).
  5. Resolve the (single) passphrase via deps.Passphrase.
  6. Open the source repo (verifies passphrase + config MAC).
  7. Open the destination store (no Open() — the dest may be empty in --init-dest mode and we'd fail before sync got to bootstrap it).
  8. Call repo.Repo.SyncTo with the appropriate options.
  9. Print the stats summary.

Refusal cases short-circuit before any S3 write.

func NewUI

func NewUI(deps UIDeps) *cobra.Command

NewUI returns the cobra command for `sentra ui`. The command loads sentra.yaml, opens the repo, builds the App via the deps, and hands it to deps.Run. Any error from Run propagates as the command's exit code.

Flags:

  • --config <path> sentra.yaml path (default ./sentra.yaml)

Future iterations might add a --readonly mode that skips the repo open and shows only static file-cached state.

func NewUnpin

func NewUnpin(deps PinDeps) *cobra.Command

NewUnpin returns the cobra command for `sentra unpin <snapshot>`.

func ResolveBuildVersion

func ResolveBuildVersion(version, commit, date string, bi func() (*debug.BuildInfo, bool)) (string, string, string)

ResolveBuildVersion resolves the identity `sentra version` reports. A release build gets these stamped by goreleaser's ldflags and they pass through untouched. A `go install` build carries the placeholders ("dev"/"none"/"unknown"), and — since go install is the one install path that needs no release pipeline — falls back to the toolchain's embedded build info: the module version (a tag or pseudo-version), the VCS revision (shortened), and the VCS commit time. bi is a seam over debug.ReadBuildInfo so tests can inject a canned build.

func SetUIAsDefault

func SetUIAsDefault(root *cobra.Command, deps UIDeps)

SetUIAsDefault rewires the root command so that bare `sentra` (no subcommand and no args) falls through to `ui`. Cobra's default behavior is to print help in that case; we want the TUI to be the default landing experience as documented in the design doc.

We do this by setting the root command's RunE: cobra invokes RunE when there is no subcommand to dispatch to. We delegate straight to the ui command's RunE so the loading / error-propagation logic stays in one place.

Important: cobra ignores RunE if the user passed an unknown subcommand — it'll print "unknown command" and exit. That's the right behavior; we only want the bare-invocation path.

Types

type AWSInspectReport

type AWSInspectReport = diag.AWSReport

AWSInspectReport is an alias for diag.AWSReport, preserved so existing DoctorDeps callers and tests keep compiling after the read-only AWS diagnostics moved to internal/diag.

func DefaultAWSInspect

func DefaultAWSInspect(ctx context.Context, cfg *config.Config) (AWSInspectReport, error)

DefaultAWSInspect performs the read-only AWS checks for `sentra doctor`. Thin wrapper over diag.Inspect.

type AgentDeps

type AgentDeps struct {
	RepoDeps

	// Actions is the registry of action verbs the orchestrator's
	// system prompt will list and the --apply path will dispatch
	// through. Production wires action.NewDefaultRegistry; tests
	// can inject a smaller registry. Nil falls back to
	// NewDefaultRegistry so a tests using zero-value AgentDeps still
	// works.
	Actions *action.Registry

	// Provider is the LLM provider passed through to the agent. Tests
	// use llm.FakeProvider; main wires the Anthropic client.
	Provider llm.Provider

	// ProviderForConfig builds the provider from the loaded command
	// config. When set, it takes precedence over Provider so --config
	// controls agent.model in production.
	ProviderForConfig func(cfg *config.Config) llm.Provider

	// Heuristics is the slice of heuristics the agent will run. Tests
	// pass deterministic stubs; main wires the production secrets /
	// large_files / etc. set.
	Heuristics []heuristics.Heuristic

	// Confirm prompts the user with a question and returns the answer.
	// Production wires this to huh.NewConfirm; tests inject a stub.
	// Skipped entirely when --yes is passed.
	Confirm func(prompt string) (bool, error)
}

AgentDeps wires the side-effecting pieces of `sentra agent scan` so tests can inject a memory store, a fake LLM Provider, and stub heuristics. Production fills these from main.go; tests drop in scripted alternatives so a Scan run is fully deterministic and never makes a real LLM call.

Provider and Heuristics are required for the scan flow itself; NewStore + Passphrase open the repo; Confirm is the per-recommendation approval prompt for --apply (skipped under --yes).

type BackupDeps

type BackupDeps struct {
	RepoDeps
	Stderr  io.Writer
	Confirm func(prompt string) (bool, error)
}

BackupDeps wires the side-effecting pieces of `sentra backup`. Production fills these with real implementations from main.go; tests inject a memory store and static passphrase.

type CheckDeps

type CheckDeps struct {
	RepoDeps
}

CheckDeps wires the side-effecting pieces of `sentra check`.

type DiffDeps

type DiffDeps struct {
	RepoDeps
}

DiffDeps wires the side-effecting pieces of `sentra diff`.

type DoctorDeps

type DoctorDeps struct {
	CheckAWSSDKIdentity  func(ctx context.Context, cfg *config.Config) error
	InspectAWS           func(ctx context.Context, cfg *config.Config) (AWSInspectReport, error)
	NewStore             func(ctx context.Context, cfg *config.Config) (blobstore.Store, error)
	PassphraseWithConfig func(cfg *config.Config) ([]byte, error)
	Stdout               io.Writer
}

DoctorDeps wires read-only diagnostics for `sentra doctor`.

type InitDeps

type InitDeps struct {
	// NewStore opens the blobstore the new repo will live in.
	// Production wires this to the S3 store; tests pass an in-memory
	// store. The cfg argument is the merged Config from sentra.yaml +
	// env overlay; for `init` on a fresh dir it's typically Defaults().
	NewStore func(ctx context.Context, cfg *config.Config) (blobstore.Store, error)

	// Passphrase returns the passphrase bytes the new repo will be
	// keyed against. Production typically wires this to
	// ui.PromptPassphraseWithConfirm; tests inject a static value so
	// the run is deterministic.
	Passphrase func() ([]byte, error)

	// Stdout receives the success summary. cobra.Command.SetOut also
	// goes here so callers can wire both in one place; if you set
	// SetOut on the returned command directly, that takes precedence.
	Stdout io.Writer
}

InitDeps wires the side-effecting pieces of `sentra init` so tests can drop in a memory store and a deterministic passphrase. The command itself just walks the lifecycle: load config (or defaults), open the store, call repo.Init, write sentra.yaml.

type LocalDeps

type LocalDeps struct {
	UI UIDeps

	// EnsureMinIO makes a local MinIO reachable before the UI launches: it
	// returns nil once MinIO answers, starts it (docker compose) if needed, and
	// returns a clear error when it can neither reach nor start it. Tests inject
	// a stub; production wires the docker/health probe in cmd/sentra.
	EnsureMinIO func(ctx context.Context) error
}

LocalDeps wires the side-effecting pieces of `sentra local`. UI is the same UIDeps the `ui` command uses; the command sets UI.SetupSeedConfig at run time so it must be passed in with SetupSeedConfig nil. EnsureMinIO is injected so tests exercise the command without touching docker or the network.

type LsDeps

type LsDeps struct {
	RepoDeps
}

LsDeps wires the side-effecting pieces of `sentra ls`.

type MCPDeps added in v0.2.0

type MCPDeps struct {
	RepoDeps
	Version        string
	PassphraseFile func() string
}

MCPDeps wires `sentra mcp`. PassphraseFile mirrors the launch probe: a func reading the live --passphrase-file flag at run time.

type PasswdDeps

type PasswdDeps struct {
	NewStore             func(ctx context.Context, cfg *config.Config) (blobstore.Store, error)
	Passphrase           func() ([]byte, error)
	PassphraseWithConfig func(cfg *config.Config) ([]byte, error)
	NewPassphrase        func(passphraseFile string) ([]byte, error)
	SavePassphrase       func(cfg *config.Config, passphrase []byte) error
	DeletePassphrase     func(cfg *config.Config) (bool, error)
	Stdout               io.Writer
}

PasswdDeps wires the side-effecting pieces of `sentra password` so tests can inject deterministic callbacks. Production wires:

  • NewStore: the standard S3-store factory (RetryStore-wrapped).
  • Passphrase: the existing old-passphrase resolver chain (--passphrase-file → SENTRA_PASSPHRASE → keyring → prompt).
  • NewPassphrase: a constrained resolver that handles ONLY the --new-passphrase-file flag and falls through to an interactive confirm-on-entry prompt. The env var SENTRA_PASSPHRASE is intentionally NOT a source for the new passphrase — env vars persist in shell history / process listings, which is the wrong default for the new secret.
  • SavePassphrase: updates the OS keyring entry after a successful rotation when passphrase.use_keyring is enabled.
  • DeletePassphrase: removes the OS keyring entry for `sentra password forget`.
  • Stdout: where the success summary lands.

The deps shape mirrors InitDeps + a NewPassphrase callback. The new callback takes the --new-passphrase-file flag value as a parameter so production wiring doesn't need to share state with the cobra command.

type PinDeps

type PinDeps struct {
	RepoDeps
}

PinDeps wires the side-effecting pieces of `sentra pin` / `unpin`.

type PolicyDeps

type PolicyDeps struct {
	RepoDeps
	Stderr io.Writer

	// OS and HomeDir steer the timer-file cleanup in remove and the
	// resync in add --replace; zero values fall back to the runtime
	// platform and home directory. Executable is the binary a
	// re-rendered timer invokes; nil means os.Executable. Runner
	// loads/unloads the timer in launchd/systemd; nil means
	// scheduler.ExecRunner, so tests must inject a fake.
	OS         string
	HomeDir    func() (string, error)
	Executable func() (string, error)
	Runner     scheduler.Runner

	// Now is the clock `run --if-due` measures the schedule against;
	// nil means time.Now.
	Now func() time.Time

	// Notify posts the desktop notification after a run. nil means OFF
	// (see notify.Runner); cmd/sentra wires notify.ExecRunner.
	Notify notify.Runner
}

PolicyDeps wires side effects for `sentra policy`.

type PruneDeps

type PruneDeps struct {
	RepoDeps

	// Confirm prompts the user with a question and returns the
	// answer. Production wires this to huh.NewConfirm; tests inject a
	// stub. Skipped entirely when --yes is passed (the Confirm
	// callback is not invoked at all in that case, so test stubs that
	// panic-on-call still work correctly under --yes).
	Confirm func(prompt string) (bool, error)
}

PruneDeps wires the side-effecting pieces of `sentra prune`. The Confirm callback is what production replaces with the huh.NewConfirm flow; tests inject a deterministic "yes/no" function so the run is scriptable.

type RecoveryKitDeps

type RecoveryKitDeps struct {
	RepoDeps
}

RecoveryKitDeps wires `sentra recovery-kit`.

type RepoDeps

type RepoDeps struct {
	NewStore             func(ctx context.Context, cfg *config.Config) (blobstore.Store, error)
	Passphrase           func() ([]byte, error)
	PassphraseWithConfig func(cfg *config.Config) ([]byte, error)
	Stdout               io.Writer
}

RepoDeps is the dependency set shared by every read-path command: the blobstore factory, the two passphrase resolvers, and the stdout sink. Commands embed it and add their own extra fields (Stderr, Confirm, ...). Exported so cmd/sentra can construct it when wiring each command.

type RestoreDeps

type RestoreDeps struct {
	RepoDeps
	Stderr io.Writer
}

RestoreDeps wires the side-effecting pieces of `sentra restore`.

type RootFlags

type RootFlags struct {
	// PassphraseFile is the path passed via --passphrase-file. Empty
	// means "no file source"; the resolver will fall through to the
	// next priority (env / keyring / prompt).
	PassphraseFile string

	// LogLevel selects the slog filter level: "debug", "info",
	// "warn", "error". Default "warn" keeps stderr quiet during
	// normal use; cron/CI runners can set --log-level=info to capture
	// completion summaries and retry events.
	LogLevel string

	// LogFormat selects the slog handler: "text" (the default,
	// human-readable) or "json" (machine-parseable, intended for
	// piping into a log aggregator).
	LogFormat string

	// LogFile redirects slog output to a file. Empty (default) sends
	// logs to stderr — except in TUI mode (`sentra ui` or bare
	// `sentra`) where stderr would corrupt the alt-screen, so the
	// TUI silently discards logs unless LogFile is set.
	LogFile string
}

RootFlags collects the values of persistent flags that subcommand bodies need to read at runtime. Wired through Deps so production and tests share the same plumbing — no globals.

Today this only carries --passphrase-file; future cross-cutting flags (a config-path override, a verbosity toggle) belong here too.

type ScheduleDeps

type ScheduleDeps struct {
	OS         string
	HomeDir    func() (string, error)
	Executable func() (string, error)
	Stdout     io.Writer

	// Now feeds the next-run computation; nil means time.Now. A seam so
	// status output is testable against a pinned clock.
	Now func() time.Time

	// Runner executes launchctl/systemctl for activation, deactivation and
	// the status query; nil means scheduler.ExecRunner. Tests MUST inject
	// a fake: the real one loads a job on the developer's machine.
	Runner scheduler.Runner
}

ScheduleDeps wires filesystem and platform details for `sentra schedule`.

type SnapshotsDeps

type SnapshotsDeps struct {
	RepoDeps
}

SnapshotsDeps wires the side-effecting pieces of `sentra snapshots`. Production fills these from main.go; tests inject a memory store and static passphrase.

type StatsDeps

type StatsDeps struct {
	RepoDeps
}

StatsDeps wires the side-effecting pieces of `sentra stats`.

type SyncDeps

type SyncDeps struct {
	NewStore             func(ctx context.Context, cfg *config.Config) (blobstore.Store, error)
	Passphrase           func() ([]byte, error)
	PassphraseWithConfig func(cfg *config.Config) ([]byte, error)
	Stdout               io.Writer
}

SyncDeps wires the side-effecting pieces of `sentra sync` so tests can inject in-memory stores and a deterministic passphrase callback. Production wires:

  • NewStore: the standard S3-store factory (RetryStore-wrapped). Called twice — once with the source's *config.Config, once with the destination's. Same factory both times.
  • Passphrase: the existing chain (--passphrase-file → env → keyring → prompt). Called EXACTLY ONCE per sync run; the resolved bytes open BOTH endpoints (clone semantic).
  • Stdout: where the success summary lands.

The deps shape mirrors PasswdDeps + a single store factory used for both ends.

type UIDeps

type UIDeps struct {
	RepoDeps

	// ProviderForConfig builds the LLM provider behind the TUI's chat
	// overlay from the loaded config. May be nil — the overlay then
	// renders a configure hint; nothing else needs it.
	ProviderForConfig func(cfg *config.Config) llm.Provider

	// Run is the actual TUI launcher. Production wires it to a
	// closure that constructs and runs a tea.Program; tests inject
	// a stub that captures the constructed App and returns nil.
	Run func(app tui.App) error

	// Version and Commit identify the build; they reach the TUI's welcome
	// splash. Plain display data, threaded from cmd/sentra. Commit may be the
	// goreleaser placeholder "none".
	Version string
	Commit  string

	// SavePassphrase re-saves a rotated passphrase to the OS keyring
	// after the TUI's password flow changes it. Same hook the `passwd`
	// command uses. May be nil when no keyring is wired.
	SavePassphrase func(cfg *config.Config, passphrase []byte) error

	// DeletePassphrase removes the OS keyring entry for the configured
	// repo — the Settings view's "forget keyring passphrase" action.
	DeletePassphrase func(cfg *config.Config) (bool, error)

	// SetupEffects overrides the setup engine's side-effecting seam. Nil
	// means runUI constructs the production setup.DefaultEffects(); tests
	// inject a fake to keep AWS/keyring calls out of the process.
	SetupEffects setup.Effects

	// SetupSeedConfig pre-fills the first-run setup wizard. When non-nil AND the
	// launch lands on the first-run path (no config file present), runUI hands
	// this config to the wizard as its starting point so the S3 detail fields
	// (bucket/prefix/region/endpoint) come up populated — the wizard reads them
	// via deps.Config → config0 → setup.DefaultPlan. It is NOT written to disk:
	// the launch stays first-run and the wizard persists the config only on
	// completion. `sentra local` sets this to MinIO coordinates; every other
	// caller (NewUI) leaves it nil for a blank wizard. Non-secret S3 coordinates
	// only — never a passphrase or credentials.
	//
	// `sentra setup` forces the wizard with a config present, so the seed's
	// first-run precondition is now an explicit !ConfigExists term in runUI
	// rather than a property of the branch.
	SetupSeedConfig *config.Config

	// PassphraseFile resolves the --passphrase-file path (the root persistent
	// flag) at run time. The launch-routing probe (probeLaunchState) must
	// honor it so a non-interactive file source resolves the same way it does
	// on every command's read path — otherwise `sentra ui --passphrase-file X`
	// against a keyring-off repo misroutes to the unlock gate, which cannot
	// read the file. It is a func, not a plain string, because production wires
	// the command deps BEFORE cobra parses argv: a value snapshot would always
	// be empty, so the probe must read the live cli.RootFlags.PassphraseFile at
	// run time. Nil (or a func returning "") means "no file source". Returns a
	// path, never a secret.
	PassphraseFile func() string
}

UIDeps wires the side-effecting pieces of `sentra ui` so tests can drop in a memory store and a stub Run callback. Run isolates the "actually launch a Bubbletea program" step behind a function so unit tests don't need a real terminal.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL