install

package
v0.32.1 Latest Latest
Warning

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

Go to latest
Published: Aug 26, 2026 License: Apache-2.0 Imports: 33 Imported by: 0

Documentation

Overview

Package install powers the `jenticctl install` onboarding wizard: an interactive form (Charm huh + lipgloss) that collects deployment + configuration choices, generates a jentic-one.yaml mirroring the app's AppConfig, then builds the stack, applies migrations, and starts the app.

The wizard is a hub-and-spoke TUI (see wizard.go): a deployment page, then a menu of configuration sections (see sections.go) each drilling into a small form. Onboarding a new AppConfig option means: add a field to Draft, add it to a Section's fields/summary, and map it in render.go.

Index

Constants

View Source
const (
	RuntimeSource = "source" // run from source (uv) on the host
	RuntimeDocker = "docker" // containerized via docker compose
)

Runtime paths the user can install onto.

View Source
const (
	BackendSQLite   = "sqlite"
	BackendPostgres = "postgres"
)

Database backends (mirrors DatabaseConfig.backend in shared/config.py).

View Source
const (
	// DefaultAppImageRepo is the published server image the Docker install path
	// pulls by default. It is the same image the release workflow builds and
	// signs (release.yml `publish-image`). Overridable via AppImageRepoEnv for
	// forks/mirrors, mirroring `make release-image REGISTRY=…`.
	DefaultAppImageRepo = "ghcr.io/jentic/jentic-one-app"

	// AppImageRepoEnv overrides the image repository (without a tag) for a fork
	// or an internal mirror, e.g. JENTIC_APP_IMAGE=my.registry/jentic-one-app.
	AppImageRepoEnv = "JENTIC_APP_IMAGE"

	// AppImageTagEnv pins the image tag (or a full @sha256: digest) to pull,
	// overriding the CLI-version → tag mapping. Mirrors the `--image-tag` flag.
	AppImageTagEnv = "JENTIC_APP_IMAGE_TAG"
)
View Source
const AdminExistsNote = "This database already has an admin account — sign in with your existing " +
	"credentials. To start completely fresh, uninstall and remove the data directory before reinstalling."

AdminExistsNote replaces FirstRunNote when a live probe finds the database already has an admin (e.g. a re-install over a database that uninstall left behind). It steers the operator to sign in rather than create an account.

View Source
const (

	// AppImageTag is the fixed local tag the CLI builds the combined app image
	// with and the generated compose file references.
	AppImageTag = "jentic-one/app:jentic-cli"
)

Docker compose service names and container-side paths. render.go emits config values relative to these so the mounted config matches the running container.

View Source
const DefaultBrokerPort = "8100"

DefaultBrokerPort is the default port the broker service binds to. It must differ from the app's server port (default 8000) since the broker runs as its own process/container.

View Source
const FirstRunNote = "First run: no users exist yet. Create the first admin with `jenticctl setup` " +
	"(or open /app/setup in the UI). There is no default password."

FirstRunNote is the post-install reminder for the no-credential first-run model: the database ships with zero users, so the operator must create the first admin before they can sign in. There is no default password to rotate.

View Source
const (
	// GitURL is the public source cloned when `jenticctl install` runs outside a
	// jentic-one checkout.
	GitURL = "https://github.com/jentic/jentic-one.git"
)
View Source
const PromptGlyph = prompt.PromptGlyph

PromptGlyph is re-exported from internal/cli/prompt.

View Source
const SrcEnv = "JENTIC_SRC"

SrcEnv lets you pin the source checkout `jenticctl install` builds from, overriding both the cwd repo-root walk and the GitHub clone fallback. Point it at a local jentic-one checkout to iterate on local changes from anywhere (and without a GITHUB_TOKEN): JENTIC_SRC=/path/to/jentic-one jenticctl install.

Variables

View Source
var (
	// FormKeyMap is re-exported from internal/cli/prompt.
	FormKeyMap = prompt.FormKeyMap
	// FormTheme is re-exported from internal/cli/prompt.
	FormTheme = prompt.FormTheme
	// NewForm is re-exported from internal/cli/prompt.
	NewForm = prompt.NewForm
	// Input is re-exported from internal/cli/prompt.
	Input = prompt.Input
	// RunConfirm is re-exported from internal/cli/prompt.
	RunConfirm = prompt.RunConfirm
	// RunForm is re-exported from internal/cli/prompt.
	RunForm = prompt.RunForm
)

The interactive-form helpers moved to internal/cli/prompt (impl/1.1 §1a) so the jentic tree can build prompts without importing this installer package. These aliases keep install's own callers (the wizard) working through the one shared, themed implementation.

View Source
var AllSurfaces = []string{"registry", "admin", "control", "auth"}

AllSurfaces are the surfaces selectable for the combined `apps` list. The broker is deliberately excluded: it cannot be bundled with other surfaces (the backend's __main__._build_app rejects `apps` containing "broker" alongside anything else), so the installer always runs it as its own service on its own port instead. See Draft.BrokerPort.

View Source
var Sections = []Section{
	componentsSection,
	databaseSection,
	authSection,
	serverSection,
	runtimeSection,
	loggingSection,
	observabilitySection,
}

Sections is the hub menu, top to bottom. Append a section to extend the wizard.

Functions

func AllDataVolumeNames

func AllDataVolumeNames() []string

AllDataVolumeNames returns the project-prefixed data volume names for BOTH install modes (SQLite and Postgres). The uninstall recovery hint uses it when the install mode is unknown (no manifest), so the operator is shown every candidate volume to remove by hand rather than being pointed at one guess.

func ComposeCreateAdmin

func ComposeCreateAdmin(w io.Writer, composePath, email, password string) error

ComposeCreateAdmin runs the one-time first-run admin creation inside a one-shot app container (`docker compose run --rm -T app python -m jentic_one create-admin`). The password is passed on the child's stdin (never argv) so it does not leak into the process table or shell history. -T keeps the run non-interactive; the Python side reads the password from stdin when not a TTY.

func ComposeDown

func ComposeDown(w io.Writer, composePath string) error

ComposeDown stops and removes the stack's containers (volumes are preserved).

func ComposeDownVolumes

func ComposeDownVolumes(w io.Writer, composePath string) error

ComposeDownVolumes stops and removes the stack's containers AND its named volumes (`down -v`). This discards the database: SQLite's data volume or the managed Postgres data dir. Use it to recover from an incompatible Postgres image upgrade (a newer major refuses a data dir initialized by an older one), which surfaces as the container failing to start. The data is unrecoverable afterwards, so callers must confirm first. If `docker` is not on PATH it returns an error immediately rather than failing deep in exec, so callers (uninstall) can surface the manual-removal hint instead of hanging.

func ComposePs

func ComposePs(composePath string) (string, error)

ComposePs returns the `docker compose ps` output for the stack.

func ComposeResetPassword

func ComposeResetPassword(w io.Writer, composePath, email, password string) error

ComposeResetPassword sets a temporary password for an existing user inside a one-shot app container, forcing a change at next sign-in. The temporary password is piped on the child's stdin (never argv) for the same reasons as ComposeCreateAdmin.

func ComposeUp

func ComposeUp(w io.Writer, composePath string) error

ComposeUp brings the stack up in detached mode.

func DaemonError

func DaemonError(check CheckResult) error

DaemonError builds an actionable error for a present-but-unresponsive Docker daemon, so the install fails fast here instead of crashing mid-build.

func DataVolumeNames

func DataVolumeNames(postgres bool) []string

DataVolumeNames returns the fully-qualified (project-prefixed) docker volume names that back the database for the given install mode, exactly as Compose creates them given the pinned project name. They are exposed so the uninstall path can point operators at the precise volume(s) to remove by hand if `docker compose down -v` could not run (e.g. the daemon was down). postgres reports the managed-Postgres volume; otherwise the SQLite data volume.

func DockerDaemonRecoveryHint added in v0.26.0

func DockerDaemonRecoveryHint() string

DockerDaemonRecoveryHint returns the canonical "how to start the Docker daemon" guidance so callers outside this package (e.g. the doctor deploy check) render the exact same advice as the fail-fast errors.

func DockerDaemonResponsiveQuick added in v0.26.0

func DockerDaemonResponsiveQuick(ctx context.Context, timeout time.Duration) (detail string, healthy bool)

DockerDaemonResponsiveQuick is a single-round-trip daemon probe for callers that must stay fast and non-blocking — `doctor` is read-only and should not hang for the full cold-start polling window when the daemon is simply down. Unlike RequireDockerDaemon it does not tolerate a cold-starting Docker Desktop; it answers within `timeout` (or when ctx is canceled). It returns a short human reason (empty when healthy) and whether the daemon answered.

func DockerNotInstalled added in v0.26.0

func DockerNotInstalled(detail string) bool

DockerNotInstalled reports whether a probe detail (from DockerDaemonResponsiveQuick) means the `docker` binary is absent rather than its daemon being down, so callers can pick DockerNotInstalledHint over DockerDaemonRecoveryHint (#954).

func DockerNotInstalledDetail added in v0.26.0

func DockerNotInstalledDetail() string

DockerNotInstalledDetail returns the exact probe reason emitted when the `docker` binary is absent — the string DockerNotInstalled recognizes. Exposed so callers/tests can reason about the binary-absent case without duplicating the literal.

func DockerNotInstalledHint added in v0.26.0

func DockerNotInstalledHint() string

DockerNotInstalledHint returns the "install Docker" guidance for the binary-absent case, mirroring DockerDaemonRecoveryHint for the daemon-down case.

func EnsureUv

func EnsureUv(w io.Writer)

EnsureUv guarantees the uv build tool is available, installing it via the official installer when it is missing, and prepending its install location (~/.local/bin) to PATH so the build step in this same process can find it.

It is best-effort: when uv cannot be bootstrapped (no curl, an unsupported platform, or an installer failure) it returns without changing anything and lets the normal preflight report uv as missing with an install hint.

func IsReleaseVersion added in v0.32.0

func IsReleaseVersion(version string) bool

IsReleaseVersion reports whether a CLI build version is a real published release (a semver the release workflow tags an image with). It is false for "dev"/"main"/a branch/a commit — the cases where the Docker pull path falls back to :latest — so callers can explain that fallback to the user.

func ManualResetCommand added in v0.32.0

func ManualResetCommand(composePath string) string

ManualResetCommand is the literal command an operator runs to discard the stack's containers and data volumes. Printed (never executed) when a migration fails over a pre-existing volume.

func MissingError

func MissingError(missing []CheckResult) error

MissingError builds an actionable error for missing required tools or unhealthy daemons.

func PullAppImage added in v0.32.0

func PullAppImage(w io.Writer, ref string) error

PullAppImage runs `docker pull <ref>`, streaming progress through w. On failure it returns an *AppImagePullError whose message carries the private-package hint (the GHCR package must be public, or a GITHUB_TOKEN / `docker login ghcr.io` is needed) — the first-release gotcha — so install fails fast with an actionable message instead of dying deep in `compose up`.

func RemoveDataVolumes

func RemoveDataVolumes(w io.Writer, names []string) (removed []string, err error)

RemoveDataVolumes removes the named docker volumes explicitly, by name. It is the belt-and-suspenders companion to ComposeDownVolumes: `docker compose down -v` only removes volumes declared in the compose file for the pinned project, so a volume created under a different project name (a pre-pinning install, a regenerated compose file, or a stack first brought up from a different directory basename) survives an otherwise-successful `down -v`. Removing the project-prefixed volume names directly closes that gap.

A volume that does not exist is treated as a no-op (not an error): the goal is "ensure gone", and `down -v` having already removed it is success, not failure. It returns the names that were actually removed so the caller can report accurately. If docker is not on PATH it returns an error immediately so the caller can surface the manual-removal hint rather than failing deep in exec.

func RenderCompose

func RenderCompose(d *Draft, cfg ComposeConfig) ([]byte, error)

RenderCompose returns the docker-compose.yaml bytes for the draft, wiring the host paths in cfg into the app (and managed Postgres) containers.

func RenderMigrateHeader

func RenderMigrateHeader(configPath string) string

RenderMigrateHeader returns a styled header for the migration step.

func RenderMigrateWarning

func RenderMigrateWarning(err error) string

RenderMigrateWarning returns a styled warning shown when migrations could not be applied (typically Postgres not yet running).

func RenderPreflight

func RenderPreflight(results []CheckResult) string

RenderPreflight returns a styled checklist of the probe results.

func RenderStartHeader

func RenderStartHeader() string

RenderStartHeader returns a styled header for the background-start step.

func RenderStartWarning

func RenderStartWarning(err error) string

RenderStartWarning returns a styled warning shown when the app could not be started in the background; the install otherwise succeeded.

func RenderSummary

func RenderSummary(d *Draft, configPath, dataDir, logsDir string, setup SetupState) string

RenderSummary returns the styled post-install summary: where the config, data, and logs live, the next-step commands for the chosen path, and onboarding notes. dataDir and logsDir may be empty if they could not be resolved. setup reflects a live /health probe so the first-admin guidance matches reality instead of always claiming "no users exist yet".

func RepoRoot

func RepoRoot() (string, bool)

RepoRoot walks up from the current directory looking for a jentic-one source checkout (a pyproject.toml that names the project, plus src/jentic_one). It returns the root and true when found. When $JENTIC_SRC is set it short-circuits to that path (validated as a real checkout) so the source is decoupled from the process's working directory.

func RequireDockerDaemon added in v0.26.0

func RequireDockerDaemon(ctx context.Context, command string) error

RequireDockerDaemon fails fast with an actionable error when the Docker daemon is not responding, so runtime commands (`start`/`stop`) surface a clear recovery path when the daemon is down (e.g. Docker Desktop closed after a reboot) instead of a raw `docker compose` transport error. The referenced command names the caller (e.g. "jenticctl start") so the recovery path points back at what the operator ran. It returns nil when the daemon answers. See jentic-one#783 and jentic-api-scorecard#224.

It only reports the problem; it deliberately does NOT start Docker itself. Client CLIs across the ecosystem (Testcontainers, act, Dagger) fail fast here rather than silently launching the daemon, since Docker Desktop is packaged as a user app, not a managed service. The recovery hint (dockerDaemonRecoveryHint) leads with a runtime-agnostic instruction so non-Docker-Desktop users aren't misled.

The probe (dockerDaemonHealth) polls for up to ~30s to tolerate a cold-starting daemon, so callers should announce the check before invoking this — otherwise the command appears to hang. See the callers in internal/cli/ctlcmd/start.go and stop.go. The ctx (the command's context) lets an operator cancel that wait with Ctrl-C (#953).

func ResetFreshDataVolumes added in v0.32.0

func ResetFreshDataVolumes(w io.Writer, composePath string, volumes []string) error

ResetFreshDataVolumes tears the stack down and removes the given data volumes. Only call this when the volumes are known to be fresh (created by the current, failed install run — see VolumeExists); it destroys the database. RemoveDataVolumes runs even if `down -v` fails: the goal is "ensure gone", and the explicit removal also covers volumes `down -v` misses (see its doc comment).

func ResolveAppImage added in v0.32.0

func ResolveAppImage(version, override string) string

ResolveAppImage returns the fully-qualified app image reference the Docker install path pulls, following the pin ladder:

override (a full ref, a bare tag, or an @sha256: digest) — from --image-tag
         or $JENTIC_APP_IMAGE_TAG
CLI build version, when it is a real semver release → :X.Y.Z
"latest" — for any non-release version ("dev", "main", a branch, a commit),
         since the release workflow only ever publishes :X.Y.Z (and :latest)

The repository defaults to DefaultAppImageRepo and is overridable via $JENTIC_APP_IMAGE. An override that already looks like a full reference (contains a registry host, i.e. a "/" before any ":") is returned verbatim so an operator can pin `ghcr.io/…@sha256:…` or a mirror exactly.

func ReuseSecrets added in v0.23.0

func ReuseSecrets(d *Draft, path string) (bool, error)

ReuseSecrets loads path (an existing jentic-one.yaml, or its uninstall backup) and pre-seeds d with its secret fields. Returns true iff at least one field was populated — the caller uses that to gate the operator notice.

A missing file returns (false, nil): a fresh install has nothing to reuse. A malformed file returns (false, err) without mutating d: the caller should warn and continue with fresh secrets so an aborted/half-written prior install doesn't brick this one.

func RunComposeMigrations

func RunComposeMigrations(w io.Writer, composePath string) error

RunComposeMigrations applies migrations via a one-shot app container (`docker compose run --rm app python -m jentic_one.migrations.run`). For Postgres, compose waits on the db healthcheck via the app's depends_on.

func RunMigrations

func RunMigrations(ctx context.Context, w io.Writer, venvPython, configPath string) error

RunMigrations applies Alembic migrations for all databases using the freshly built venv interpreter, pointed at the generated config. Output is streamed to w. The runner is cwd-independent (it loads packaged migration scripts).

ctx is threaded through to exec.CommandContext (F2, review round-3 #7) so a Ctrl-C during a long migration cancels the Python child instead of orphaning it — which, against a live DB mid-rollback, is the worst input to the rollback path. Callers hold the command's signal-cancelled context; pass it here.

func RunWizard

func RunWizard(d *Draft, hdr Header) (bool, error)

RunWizard runs the interactive install wizard, mutating d in place. It returns true when the user chose Continue (proceed with the install), false if they quit/cancelled. hdr supplies the CLI/server versions shown in the top-right.

func StartApp

func StartApp(venvPython, configPath, logPath, pidPath string) (int, error)

StartApp launches `<venvPython> -m jentic_one` in the background, detached from the installer, with output redirected to logPath and the PID written to pidPath. It returns the PID once the app has survived a short startup window; if the app exits immediately it returns an error containing the log tail.

func StartBroker

func StartBroker(venvPython, configPath, logPath, pidPath, brokerPort string) (int, error)

StartBroker launches the broker as its own background process with apps=broker on its dedicated port (the broker cannot be bundled with other surfaces). It mirrors StartApp but overrides JENTIC__APPS and the server port via the environment so it can run alongside the combined app.

func UnqualifiedPublishes added in v0.32.0

func UnqualifiedPublishes(data []byte) ([]string, error)

UnqualifiedPublishes parses docker-compose.yaml bytes and returns a line per port mapping published without a host-IP prefix, as "<service> <mapping>".

Docker publishes an unqualified mapping ("8000:8000") on ALL interfaces — and bypasses UFW while doing it — so on an install whose operator chose a loopback bind this is a silent network exposure (#992). Compose files generated after the #992 fix always carry a prefix; this check exists for the installs generated before it.

func ValidateDraft added in v0.32.0

func ValidateDraft(d *Draft) error

ValidateDraft applies the wizard's field validators to a headless draft, so --defaults/--answers installs are held to exactly the interactive rules. The error names the offending answer-file key.

func VenvCreateAdmin

func VenvCreateAdmin(w io.Writer, venvPython, configPath, email, password string) error

VenvCreateAdmin runs first-run admin creation against a local (non-Docker) install by invoking the venv Python directly. The password is piped on stdin for the same reasons as ComposeCreateAdmin.

func VenvPython

func VenvPython(venvDir string) string

VenvPython returns the python interpreter path inside the given venv dir.

func VenvResetPassword

func VenvResetPassword(w io.Writer, venvPython, configPath, email, password string) error

VenvResetPassword sets a temporary password for an existing user against a local (non-Docker) install by invoking the venv Python directly. The password is piped on stdin for the same reasons as ComposeResetPassword.

func VolumeExists added in v0.32.0

func VolumeExists(name string) (bool, error)

VolumeExists reports whether the named docker volume exists. "No such volume" is a clean false; any other inspect failure (daemon down, permission denied) is an error so callers do not mistake "could not tell" for "fresh" — that mistake would make the recovery path destroy a real database.

func WriteComposeArtifacts

func WriteComposeArtifacts(d *Draft, cfg ComposeConfig) error

WriteComposeArtifacts renders and writes the compose file and ensures the bind-mount directories exist.

Modes are chosen for the container uids that must read/write them (#992): the app/broker images run as the unprivileged `jentic` user (uid 999, see deploy/docker/*.Dockerfile) and postgres runs as uid 999 too — neither matches the installing host user, so anything bind-mounted into a container must be world-readable. Host-side protection comes from ~/.jentic itself being 0700. The compose file stays 0600: only the docker CLI (running as the host user) reads it, and it can carry database credentials.

Types

type Answers added in v0.32.0

type Answers struct {
	// Deployment: "docker" (default) or "source".
	Runtime *string `yaml:"runtime"`

	// Components: the enabled surfaces (registry/admin/control/auth).
	Apps *[]string `yaml:"apps"`

	// Database: "postgres" (default) or "sqlite".
	Database   *string `yaml:"database"`
	PGHost     *string `yaml:"pg_host"`
	PGPort     *string `yaml:"pg_port"`
	PGName     *string `yaml:"pg_name"`
	PGUser     *string `yaml:"pg_user"`
	PGPassword *string `yaml:"pg_password"`
	// PGExpose publishes the managed Postgres 5432 on the host (Docker path
	// only; off by default — see #992).
	PGExpose  *bool   `yaml:"pg_expose_host_port"`
	SQLiteDir *string `yaml:"sqlite_dir"`

	// Server.
	BindHost   *string `yaml:"bind_host"`
	AppPort    *string `yaml:"app_port"`
	BrokerPort *string `yaml:"broker_port"`

	// Auth.
	BaseURL         *string `yaml:"base_url"`
	SSOEnabled      *bool   `yaml:"sso_enabled"`
	SSOClientID     *string `yaml:"sso_client_id"`
	SSOClientSecret *string `yaml:"sso_client_secret"`

	// Runtime knobs.
	Debug    *bool   `yaml:"debug"`
	LogLevel *string `yaml:"log_level"`

	// Logging.
	LogFile     *bool   `yaml:"log_file"`
	LogFileName *string `yaml:"log_file_name"`

	// Observability.
	Metrics *string `yaml:"metrics"`
	Tracing *string `yaml:"tracing"`
}

Answers mirrors the wizard-editable Draft fields for the --answers file. Pointer fields distinguish "not provided" (keep the default) from an explicit zero value ("debug: false"). Field names are the wizard's own vocabulary, grouped as the hub sections present them.

func LoadAnswers added in v0.32.0

func LoadAnswers(path string) (*Answers, error)

LoadAnswers reads and strictly decodes an answers file: unknown keys are an error (a typoed key silently keeping a default would defeat the point of an unattended install).

func (*Answers) Apply added in v0.32.0

func (a *Answers) Apply(d *Draft)

Apply overlays the provided answers onto the draft. Only fields present in the file are touched; everything else keeps the NewDraft default.

type AppImagePullError added in v0.32.0

type AppImagePullError struct {
	Ref string
	Err error
}

AppImagePullError wraps a `docker pull` failure with the actionable hint that the GHCR package may be private (the first-release checklist gotcha).

func (*AppImagePullError) Error added in v0.32.0

func (e *AppImagePullError) Error() string

func (*AppImagePullError) Unwrap added in v0.32.0

func (e *AppImagePullError) Unwrap() error

type BuildPlan

type BuildPlan struct {
	// SourceDir is the source checkout to install from (the local repo root, or
	// the clone target when FromGit is true).
	SourceDir string
	// VenvDir is the virtualenv to create under ~/.jentic.
	VenvDir string
	// FromGit reports whether the source must be cloned from GitHub first.
	FromGit bool
	// GitURL is the clone source (set when FromGit is true).
	GitURL string
	// Ref pins the git ref (tag, branch, or commit) the source is synced to
	// before building. Empty means "track the remote's default branch". When set,
	// the build MUST land on exactly this ref or fail — silently building
	// something else is how `update --ref vX.Y.Z` used to produce a stack built
	// from main (#949).
	Ref string
	// RefPinned records that Ref came from an explicit `--ref`, rather than being
	// the release tag `update` resolves on its own. Only an operator's explicit
	// pin is worth reporting as "ignored" when it cannot be applied; saying that
	// about a ref they never typed reads as if their input was discarded.
	RefPinned bool
}

BuildPlan describes how the local virtualenv will be built.

func PlanLocalBuild

func PlanLocalBuild(venvDir, cloneDir string) BuildPlan

PlanLocalBuild decides whether to build from a local checkout or to clone the source from GitHub first, based on whether the CLI runs inside the repo.

func (BuildPlan) AtRef added in v0.26.0

func (p BuildPlan) AtRef(ref string, pinned bool) BuildPlan

AtRef returns a copy of the plan targeting ref. pinned distinguishes an explicit `--ref` from a ref the caller resolved itself. An empty ref is a no-op, so callers can pass through whatever they resolved without branching.

func (BuildPlan) BuildImages

func (p BuildPlan) BuildImages(w io.Writer) error

BuildImages builds the shared python-base (builder + runtime) stages and the combined app image tagged AppImageTag, from the plan's source checkout (cloning first when FromGit). Output streams to w. The python-base targets must be built first since app.Dockerfile starts FROM python-base:runtime and copies the wheel from python-base:builder.

func (BuildPlan) Execute

func (p BuildPlan) Execute(w io.Writer) error

Execute performs the build, streaming command output to w. On success the caller should record VenvPython() into the Draft.

func (BuildPlan) PinnedRefIgnored added in v0.26.0

func (p BuildPlan) PinnedRefIgnored() bool

PinnedRefIgnored reports whether an explicitly requested ref cannot be honoured because the build reads a local checkout rather than a managed clone. The working tree belongs to the operator, so syncing it to a ref would clobber their work; the caller must surface this instead of implying the ref was used.

func (BuildPlan) RenderDockerBuildHeader

func (p BuildPlan) RenderDockerBuildHeader() string

RenderDockerBuildHeader returns a styled description of the image build.

func (BuildPlan) RenderHeader

func (p BuildPlan) RenderHeader() string

RenderHeader returns a styled description of what the build will do.

func (BuildPlan) VenvPython

func (p BuildPlan) VenvPython() string

VenvPython returns the python interpreter path inside this plan's venv.

type CheckResult

type CheckResult struct {
	Req     Requirement
	Found   bool
	Path    string
	Version string
	// MissingWhy, when set by a custom probe, overrides Req.Why in the rendered
	// MISSING row (a probe-specific recovery hint).
	MissingWhy string
	// DaemonChecked is true when this requirement carries a daemon-health probe
	// (only `docker`). Healthy/DaemonDetail are meaningful only when true.
	DaemonChecked bool
	// Healthy reports whether the daemon answered (`docker info` succeeded).
	Healthy bool
	// DaemonDetail is a short human reason when the daemon is unhealthy.
	DaemonDetail string
}

CheckResult is the outcome of probing a single Requirement.

func Missing

func Missing(results []CheckResult) []CheckResult

Missing returns the checks whose tool was not found, or whose daemon probe failed.

func Preflight

func Preflight(ctx context.Context, d *Draft) []CheckResult

Preflight probes every Requirement for the chosen path. ctx bounds the Docker daemon probe so a cold-start wait can be canceled (Ctrl-C).

func UnhealthyDaemon

func UnhealthyDaemon(results []CheckResult) (CheckResult, bool)

UnhealthyDaemon returns the docker check whose daemon probe failed, if any. (The binary is present but the daemon did not answer.)

type ComposeConfig

type ComposeConfig struct {
	// ComposePath is where the generated docker-compose.yaml is written.
	ComposePath string
	// ConfigHostPath is the generated jentic-one.yaml mounted read-only at
	// containerConfigPath.
	ConfigHostPath string
	// LogsHostDir is bind-mounted at containerLogsDir for the file log sink.
	LogsHostDir string
	// AppImage is the app/broker container image reference the generated
	// compose file uses. Empty means "local build": RenderCompose falls back to
	// AppImageTag (the tag the from-source path builds). A pull-by-default
	// install sets it to the resolved published ref
	// (ghcr.io/jentic/jentic-one-app:<ver>) via ResolveAppImage.
	AppImage string
}

ComposeConfig carries the host-side paths the generated compose stack binds into the containers. All paths should be absolute.

type Draft

type Draft struct {
	// RuntimePath selects how the stack runs: from source or in containers.
	RuntimePath string

	// DBBackend selects the database backend for every surface.
	DBBackend string

	// Postgres connection (used when DBBackend == BackendPostgres). All surfaces
	// share one server; they are isolated by schema_name.
	PGHost     string
	PGPort     string
	PGName     string
	PGUser     string
	PGPassword string

	// PGExposeHostPort publishes the managed Postgres container's 5432 on the
	// host (Docker path only). Off by default: the app and broker reach the
	// database over the compose network, so a host publish is purely a
	// debugging/tooling convenience — and it was one of #992's exposure
	// surfaces. When enabled, PGPort is the published host port.
	PGExposeHostPort bool

	// SQLiteDir is the directory holding per-surface *.db files (BackendSQLite).
	SQLiteDir string

	// Apps is the set of enabled surfaces (maps to AppConfig.apps).
	Apps []string

	// Server binding (maps to AppConfig.server).
	ServerHost string
	ServerPort string

	// BrokerPort is the host/bind port for the broker service. The broker runs
	// as its own process (source) or container (Docker) with apps=[broker],
	// since it cannot share the combined app. It must differ from ServerPort.
	BrokerPort string

	// Runtime knobs (maps to AppConfig.runtime).
	Debug    bool
	LogLevel string

	// File logging sink (maps to AppConfig.logging). When LogFileEnabled the app
	// mirrors its logs as one JSON object per line to LogFileDir/LogFileName, in
	// addition to stdout (which the CLI captures separately). LogFileDir is set
	// to the absolute ~/.jentic/logs by the install command before rendering.
	LogFileEnabled bool
	LogFileDir     string
	LogFileName    string

	// Observability exporters (maps to AppConfig.observability).
	MetricsExporter string
	TracingExporter string

	// Auth (maps to AppConfig.auth). AuthBaseURL overrides canonical_base_url;
	// when empty it is derived from the server binding.
	AuthBaseURL string

	// SSO / external IdP. When SSOEnabled, the config gains an auth.idp block
	// (Google) and a freshly generated id_signing key (mirrors
	// config/local-sso.yaml).
	SSOEnabled      bool
	SSOClientID     string
	SSOClientSecret string

	// IDSigning is the ES256 key the platform signs its own ID tokens with. The
	// PEM is generated by FillSecrets when SSOEnabled.
	IDSigningKID    string
	IDSigningKeyPEM string

	// Generated secrets, populated by FillSecrets before rendering. Never
	// prompted for; always freshly generated so installs are secure by default.
	//
	// On a reinstall over an existing config or its uninstall backup, ReuseSecrets
	// pre-seeds these fields so FillSecrets (fill-only-empty) leaves them alone
	// and the config's on-disk data stays readable across the rewrite.
	EncryptionKey      string
	AdminJWTSecret     string
	AdminInvitePepper  string
	ConnectStateSecret string

	// EncryptionKeyset is a verbatim carry-over of an existing config's
	// `credentials.encryption` block, populated by ReuseSecrets on a reinstall.
	// When non-nil, render.go writes it out unchanged; EncryptionKey is
	// ignored. Preserves a hand-rotated multi-key keyset (active_id: v2 +
	// v1/v2 entries) — flattening it back to a single v1 entry would silently
	// invalidate rows encrypted with a retired key on the next rotation. Nil
	// on a fresh install; render.go emits the current default single-v1
	// layout from EncryptionKey in that case.
	EncryptionKeyset *encryptionOut

	// Telemetry consent decision, stamped onto the draft by the install command
	// (from the consent prompt) before rendering. When TelemetryEnabled is true
	// a stable opaque TelemetryInstanceID is generated and both are written into
	// the generated jentic-one.yaml so the backend's telemetry gate actually
	// reflects the user's choice. When false the config still carries an explicit
	// `enabled: false` so the decision is recorded rather than left absent.
	TelemetryEnabled    bool
	TelemetryInstanceID string

	// VenvPython is the interpreter path of the venv built for the local path.
	// Set after a successful build; when present the next-step commands use it
	// directly instead of `make install` + `uv run`.
	VenvPython string

	// StackRef is the git ref the stack was actually built from, when the build
	// synced the managed clone to one (build-local from git). Empty for a pulled
	// release image or a local-checkout build; the manifest then records the CLI
	// version as before. Recording the real ref keeps `jenticctl update`'s
	// tracking honest when the stack was built from a branch/tag/commit.
	StackRef string

	// MigrationsDone reports whether the wizard already applied migrations. When
	// true the next steps skip the migrate command and only cover starting the app.
	MigrationsDone bool

	// App background-start results (local path). When AppStarted is true the
	// wizard launched the app for you and the summary shows how to view/stop it.
	AppStarted bool
	AppPID     int

	// BrokerStarted / BrokerPID mirror AppStarted/AppPID for the broker process
	// launched in the background on the local (source) path.
	BrokerStarted bool
	BrokerPID     int

	// ComposePath is the generated docker-compose file path (Docker path only).
	// Set after the compose artifacts are written so the summary can point at it.
	ComposePath string
}

Draft holds every answer collected by the wizard. Fields are bound directly to huh form fields (hence the string-typed ports), then translated into the on-disk config by render.go.

func NewDraft

func NewDraft() *Draft

NewDraft returns a Draft pre-populated with sensible local-development defaults (mirroring config/local.yaml).

func (*Draft) BaseURL

func (d *Draft) BaseURL() string

BaseURL is the canonical control-plane URL derived from the server binding. A 0.0.0.0 bind is reported as 127.0.0.1 since that is the reachable address.

func (*Draft) BrokerURL

func (d *Draft) BrokerURL() string

BrokerURL is the local URL the broker service listens on, derived from the server bind host and the dedicated broker port.

func (*Draft) CanonicalBaseURL

func (d *Draft) CanonicalBaseURL() string

CanonicalBaseURL is the auth canonical_base_url: the explicit override when set, otherwise the URL derived from the server binding.

func (*Draft) FillSecrets

func (d *Draft) FillSecrets() error

FillSecrets populates the Draft's generated-secret fields with fresh random values. The encryption key is a 32-byte (AES-256) base64 string, matching the credentials.encryption keyset format. The other secrets are random tokens suitable for local installs. Call once before rendering the config.

Fields that already carry a value are preserved: ReuseSecrets populates them from an existing config so a reinstall keeps the on-disk data readable (a rotated encryption key would silently brick stored credentials). Only blank fields are filled here. The `--fresh-secrets` install flag skips the reuse step entirely and re-enters this with a zero draft, giving the old "always fresh" behavior for deliberate rotation.

func (*Draft) IsDocker

func (d *Draft) IsDocker() bool

IsDocker reports whether the containerized runtime path was chosen.

func (*Draft) IsPostgres

func (d *Draft) IsPostgres() bool

IsPostgres reports whether the Postgres backend was chosen.

func (*Draft) NextSteps

func (d *Draft) NextSteps(configPath string, setup SetupState) []Step

NextSteps returns the ordered shell steps to finish bringing the stack up for the chosen path. The installer runs the build/migrate/start itself; these are the remaining manual steps (e.g. when --skip-build or --no-start was used). configPath is the path the generated config was written to. setup reflects a live /health probe so the create-first-admin step is only shown when the DB actually has no users.

func (*Draft) OAuthCallbackURL

func (d *Draft) OAuthCallbackURL() string

OAuthCallbackURL returns the redirect URI for the direct_oauth2 credential provider, derived from the canonical base URL and the control surface callback path.

func (*Draft) PublishHost added in v0.32.0

func (d *Draft) PublishHost() string

PublishHost is the host interface Docker publishes container ports on, derived from the wizard's bind-host answer. It exists because the Docker path has TWO distinct binds that must not be conflated: the in-container process bind (always 0.0.0.0 so the published port is reachable — see render.go toConfig) and the host-side publish address, which must honour the user's choice. An unqualified compose port mapping publishes on all interfaces, so omitting this prefix would silently expose a loopback-intended install to the network (#992).

localhost is normalized to 127.0.0.1 (Docker requires an IP for the host prefix); empty defaults to loopback; 0.0.0.0 passes through as the user's explicit choice to publish on all interfaces.

func (*Draft) Render

func (d *Draft) Render() ([]byte, error)

Render returns the jentic-one.yaml bytes for this Draft. Call FillSecrets first so the secret fields are populated.

type Header struct {
	CLIVersion    string
	ServerVersion string
	ServerRunning bool
}

Header carries the version metadata shown in the wizard's top-right panel: the CLI's own version and the server's version when one is already running.

type PinnedBanner

type PinnedBanner struct {
	// contains filtered or unexported fields
}

PinnedBanner keeps the jentic banner fixed at the top of the terminal while build output scrolls in the region beneath it, using a DECSTBM scroll region. When the output is not an interactive terminal it is inert and Stop is a no-op.

func StartPinnedBanner

func StartPinnedBanner(out *os.File) *PinnedBanner

StartPinnedBanner clears the screen, draws the banner at the top, and confines further output to a scroll region below it. It degrades gracefully: if out is not a TTY (or is too short) it returns an inert controller and the caller's output prints normally.

func (*PinnedBanner) Stop

func (p *PinnedBanner) Stop()

Stop releases the scroll region and moves the cursor below it so subsequent output (the summary) flows normally.

type ProbeResult added in v0.32.0

type ProbeResult struct {
	Found bool
	// Detail is shown next to an OK row (a version/path); optional.
	Detail string
	// MissingWhy overrides Req.Why in the MISSING row when non-empty, so a
	// custom probe can give a more precise recovery hint than the static Why.
	MissingWhy string
}

ProbeResult is what a custom Requirement.Probe returns.

type Requirement

type Requirement struct {
	// Name is the executable looked up on PATH.
	Name string
	// Why is a short reason the tool is needed.
	Why string
	// URL is an install hint shown when the tool is missing.
	URL string
	// Soft marks a requirement whose absence is a warning, not a failure:
	// RenderPreflight still shows it, but Missing() excludes it from the set
	// that fails the install (e.g. npm — the UI build is skipped, not fatal).
	Soft bool
	// Probe, when set, replaces the default exec.LookPath check with custom
	// logic (returns found, an optional path/version detail, and an optional
	// override for the "why it's missing" hint). Used for checks that aren't a
	// bare "is this binary on PATH" — e.g. "can uv resolve a Python 3.12
	// interpreter" or "is a source checkout / token available for the clone".
	Probe func() ProbeResult
}

Requirement is an external tool the install needs on PATH. Extend requirementsFor to add new ones for a given install path.

type SchemaState added in v0.26.0

type SchemaState int

SchemaState is the migration state of the stack's databases.

const (
	// SchemaUnknown means the state could not be determined — the check could
	// not run at all (no docker, an app image predating `--check`, an
	// unreachable database). Callers must treat it as "carry on": refusing to
	// start because a *diagnostic* failed would be a worse regression than the
	// bug the check exists to catch.
	SchemaUnknown SchemaState = iota
	// SchemaCurrent means every database is at head.
	SchemaCurrent
	// SchemaUninitialized means at least one database has no schema at all
	// (no Alembic version table). A wiped or brand-new volume. There is no data
	// to protect, so creating the schema is safe and needs no confirmation.
	SchemaUninitialized
	// SchemaPending means at least one database has a schema but is behind
	// head. It holds data that forward-only migrations will rewrite, so this is
	// the operator's call to make with a backup in hand — never automatic.
	SchemaPending
)

func ComposeSchemaState added in v0.26.0

func ComposeSchemaState(w io.Writer, composePath string) SchemaState

ComposeSchemaState reports whether the stack's databases are migrated, without modifying them. It runs `migrations.run --check` in a one-shot app container.

It deliberately returns no error. Any failure to *obtain* an answer collapses to SchemaUnknown, because a caller cannot act on a non-answer: blocking a start because a diagnostic broke would be worse than the problem being diagnosed. Only a verdict the runner actually printed is authoritative.

w receives progress and, when no verdict could be read, the check's own output. The probe pulls/starts a database container, so it is not instant; without a line explaining the pause `start` looks hung. And silently swallowing the output would make the one case that needs explaining — "why did it not notice?" — undiagnosable.

type Section

type Section struct {
	// ID is a stable identifier (used in logs/tests).
	ID string
	// Title is the row label and editor header.
	Title string
	// Blurb is a one-line description shown in the detail pane.
	Blurb string
	// Groups builds the editor form (grouped so conditional fields can hide).
	Groups func(d *Draft) []*huh.Group
	// Summary returns the "current values" lines shown in the detail pane.
	Summary func(d *Draft) []string
}

Section is one configurable area on the wizard hub. Each section renders a detail pane (Blurb + Summary of current values) and, when selected, a small form of Groups bound into the Draft.

type SetupState

type SetupState int

SetupState is a tri-state describing whether the freshly installed stack still needs its first admin account, resolved from a live /health probe. It lets the summary tell the truth instead of unconditionally asserting "no users exist yet" — which is false when a re-install reuses a database that already has an admin (e.g. uninstall left the SQLite file behind).

const (
	// SetupUnknown means the installer could not determine the state (no probe,
	// or the probe failed); fall back to the generic first-run guidance.
	SetupUnknown SetupState = iota
	// SetupRequired means the database has no users yet — create the first admin.
	SetupRequired
	// SetupComplete means an admin already exists — sign in instead of creating.
	SetupComplete
)

type Step

type Step struct {
	Title    string
	Commands []string
}

Step is a single labelled group of shell commands to run next.

Jump to

Keyboard shortcuts

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