preview

package
v0.2.8 Latest Latest
Warning

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

Go to latest
Published: Aug 17, 2026 License: AGPL-3.0 Imports: 24 Imported by: 0

Documentation

Overview

Package preview resolves, spawns, and supervises per-worktree development servers on a Clank host.

Expo retains its built-in detection and bootstrap. Arbitrary web servers use the strict launch configuration loaded by internal/launchconfig; project discovery and command selection happen once in a connected-agent setup task, not in this package.

Manager keeps running processes keyed by (worktree ID, configured service name). Start allocates a loopback port, exports it as PORT, spawns the command, and returns StateStarting while an HTTP probe transitions it to Ready or Failed. Stop, idle reaping, and Shutdown terminate the process group.

When configured, GWClient registers the selected service and internal port with the cloud gateway. The gateway owns multi-tenant authentication and the external reverse-proxy route; clank-host never proxies preview traffic.

GWClient is the sprite-side HTTP client for the gateway's /webhooks/preview/{register,revoke} endpoints. Manager calls Register after Metro reaches Ready and Revoke on Stop/Shutdown /reap so the gateway's preview_routes table reflects sprite reality.

Bearer auth: the gateway authenticates via the per-host notifier_token (same one used for /webhooks/notifications). One credential, two webhooks.

Lifecycle:

  • Construct via NewGWClient(baseURL, bearer). Empty baseURL = "no gateway wired" (laptop dev path); methods become no-ops that succeed without making HTTP calls.
  • All methods take a context for cancellation. They surface HTTP transport / status errors directly; Manager logs and continues rather than blocking start/stop on webhook flakiness.

Package-level docs live in doc.go.

Index

Constants

View Source
const (
	DefaultIdleTimeout = 15 * time.Minute
	DefaultStopGrace   = 3 * time.Second
)

Default lifecycle timers. Bumpable via Options for tests.

Variables

View Source
var (
	// ErrSetupRequired reports that a web project needs one-time launch setup.
	ErrSetupRequired = errors.New("preview: launch setup is required")
	// ErrInvalidLaunchConfig reports a present but unusable launch file.
	ErrInvalidLaunchConfig = errors.New("preview: launch configuration is invalid")
)
View Source
var ErrNotRunning = errors.New("preview: no preview server running for worktree")

ErrNotRunning is returned by Stop when no server exists for the worktree. Mapped to 404.

Functions

This section is empty.

Types

type GWClient

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

GWClient is the gateway webhook client. Nil-safe: a nil receiver makes every method a no-op success. Used in tests + laptop dev.

func NewGWClient

func NewGWClient(baseURL, bearer string) *GWClient

NewGWClient builds a GWClient. Pass empty baseURL to disable the integration entirely (no webhook calls; Register returns a zero RegisterResponse). bearer is the per-host notifier_token.

func (*GWClient) Enabled

func (c *GWClient) Enabled() bool

Enabled reports whether the client will actually make HTTP calls. Lets Manager log a "running without gateway integration" warning on cold start without touching the no-op control flow.

func (*GWClient) Register

func (c *GWClient) Register(ctx context.Context, req RegisterRequest) (RegisterResponse, error)

Register calls /webhooks/preview/register. When the client is disabled (no baseURL), returns a zero RegisterResponse and nil error — Manager treats this as "preview is running but no public URL was minted" so callers can still introspect Status.

func (*GWClient) Revoke

func (c *GWClient) Revoke(ctx context.Context, req RevokeRequest) error

Revoke calls /webhooks/preview/revoke. Best-effort: gateway also idempotently no-ops on unknown (host, wid, svc) triples, so a duplicate Revoke from a flaky network won't surface as an error.

type Kind

type Kind string

Kind tags what flavor of dev server a Spec describes. Drives the client's render decision once the server is up: KindExpo is consumed by clank-mobile (QR + phone), KindWeb by `clank preview`'s browser flow, which fronts the dev server with the overlay-injecting proxy in internal/webpreview instead of printing a QR.

const (
	KindExpo Kind = "expo"
	KindWeb  Kind = "web"
)

type Manager

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

Manager owns the per-(worktree, service) dev-server registry and exposes the lifecycle (Start/Stop/Status). One Manager lives on each host.Service.

func New

func New(opts Options) *Manager

New constructs and starts a Manager. Call Shutdown to release the reaper goroutine.

func (*Manager) LogTail

func (m *Manager) LogTail(worktreeID, workDir string) []byte

LogTail returns the default service's stdout/stderr tail: Expo's fixed service, or a configured web project's default launch entry — resolved the same way Start and StatusNamed pick the default, so an unnamed configured preview's logs aren't silently empty.

func (*Manager) LogTailNamed

func (m *Manager) LogTailNamed(worktreeID, serviceName string) []byte

LogTailNamed returns one named service's stdout/stderr tail.

func (*Manager) Shutdown

func (m *Manager) Shutdown()

Shutdown stops every running server (and revokes its token) plus the reaper goroutine. Idempotent.

func (*Manager) Start

func (m *Manager) Start(ctx context.Context, worktreeID, workDir, launchName string) (Status, error)

Start spawns the dev server for (worktreeID, serviceName) and returns its Status. Idempotent — a second Start for the same (wid, service) returns the existing server's snapshot without re-spawning.

After the spawn passes readiness, Start calls GWClient.Register to mint the public token + URL and stores those on the running record so subsequent Status calls surface them. When GWClient is nil or disabled, Status.Token/URL stay empty (laptop dev path).

Returns ErrSetupRequired when a non-Expo project has no launch config.

func (*Manager) Status

func (m *Manager) Status(ctx context.Context, worktreeID, workDir string) (Status, error)

Status resolves Expo or the configured default launch.

func (*Manager) StatusNamed

func (m *Manager) StatusNamed(_ context.Context, worktreeID, workDir, launchName string) (Status, error)

StatusNamed returns one named launch or its current running service.

func (*Manager) Stop

func (m *Manager) Stop(worktreeID string) error

Stop terminates every dev server registered under worktreeID. Blocks until each process tree is reaped. Returns ErrNotRunning when no services exist for the worktree.

func (*Manager) StopService

func (m *Manager) StopService(worktreeID, serviceName string) error

StopService terminates one named preview without affecting sibling services.

type Options

type Options struct {
	// IdleTimeout is how long a running server can go without proxy
	// traffic before the reaper stops it. Zero uses DefaultIdleTimeout.
	IdleTimeout time.Duration

	// StopGrace is the SIGTERM→SIGKILL window. Zero uses DefaultStopGrace.
	StopGrace time.Duration

	// Log is the logger; nil falls back to the default logger.
	Log *log.Logger

	// GWClient mints + revokes public tokens with the gateway. Nil
	// (or a disabled client) leaves Status.Token/URL empty — useful
	// for laptop dev where there's no gateway to register with.
	GWClient *GWClient
}

Options configures a Manager. Each field is optional with a sensible default — pass Options{} for a bare manager.

type ReadyProbe

type ReadyProbe struct {
	Path           string
	ExpectedSubstr string
}

ReadyProbe describes the HTTP-readiness check. Manager.spawn polls http://127.0.0.1:<port><Path> with the eventual public Host header until the response is 200 AND the body contains ExpectedSubstr (or always, if empty). The probe times out at the package default unless overridden via spawnRequest.ReadyTimeout.

For Expo, Metro exposes /status returning "packager-status:running" — see detect.go.

type RegisterRequest

type RegisterRequest struct {
	WorktreeID   string `json:"worktree_id"`
	ServiceName  string `json:"service_name"`
	InternalPort int    `json:"internal_port"`
}

RegisterRequest mirrors gateway/webhook_preview.go's previewRegisterRequest. Kept here as its own type so the sprite doesn't depend on the gateway package — the two contracts agree via JSON tags, not Go-level type sharing.

type RegisterResponse

type RegisterResponse struct {
	Token      string            `json:"token"`
	URL        string            `json:"url"`
	Visibility tokens.Visibility `json:"visibility"`
	ExpiresAt  time.Time         `json:"expires_at"`
}

RegisterResponse is what the gateway returns. visibility starts at owner_only on first register; mobile/owner flips it later via the owner-facing /v1/preview/tokens/{token}/share endpoint.

type RevokeRequest

type RevokeRequest struct {
	WorktreeID  string `json:"worktree_id"`
	ServiceName string `json:"service_name"`
}

RevokeRequest is the body for /webhooks/preview/revoke.

type SetupRequiredError

type SetupRequiredError struct {
	ProjectConfigPath string
	Prompt            string
}

SetupRequiredError carries the one-time connected-agent setup contract.

func (*SetupRequiredError) Error

func (e *SetupRequiredError) Error() string

func (*SetupRequiredError) Unwrap

func (e *SetupRequiredError) Unwrap() error

type Spec

type Spec struct {
	// Kind identifies which client integration to use.
	Kind Kind

	// CmdTemplate is the argv template. "%d" is replaced with the
	// allocated port only when ShouldSubstitutePort is true.
	CmdTemplate []string

	// ShouldSubstitutePort enables the legacy Expo argv template. Configured
	// web commands receive the port only through the PORT environment variable.
	ShouldSubstitutePort bool

	// Environment contains configured web environment values. Clank expands its
	// runtime placeholders immediately before spawning the child process.
	Environment map[string]string

	// ReadyProbe is the HTTP poll Manager runs after spawn to flip
	// State from Starting to Ready. Concrete contract beats stdout-
	// scanning: the probe only passes when the dev server is actually
	// serving traffic, vs. the print-then-bind race we had earlier
	// with substring matching.
	ReadyProbe ReadyProbe
}

Spec is the normalized recipe Manager uses to spawn a detected Expo or configured web development server. It is internal and never serialized.

func Detect

func Detect(workDir string) (*Spec, error)

Detect inspects workDir and returns a Spec if it looks like an Expo app. Web launch behavior is intentionally not inferred here; it comes from the strict launch configuration generated during one-time setup. The contract:

  • (nil, nil) means "not previewable" — a normal answer, NOT an error. Surface it as preview_available: false / available: false to the client.
  • (nil, err) means I/O blew up reading the worktree. The caller should log and treat as not-previewable, but a flood of these signals a real problem (disk, perms, racy worktree removal).
  • (*Spec, nil) means the dev server should be spawnable with the returned recipe.

Detection is intentionally cheap (one Stat for package.json, one small JSON parse, up to three more Stats for app.config files) so callers can run it per worktree-list row without caching.

type State

type State string

State is the lifecycle state of a running server. Status responses expose this verbatim so the mobile loading screen can drive its UI.

const (
	StateStopped  State = "stopped"
	StateStarting State = "starting"
	StateReady    State = "ready"
	StateFailed   State = "failed"
)

type Status

type Status struct {
	Available         bool       `json:"available"`
	SetupRequired     bool       `json:"setup_required,omitempty"`
	SetupPrompt       string     `json:"setup_prompt,omitempty"`
	ProjectConfigPath string     `json:"project_config_path,omitempty"`
	Kind              Kind       `json:"kind,omitempty"`
	ServiceName       string     `json:"service_name,omitempty"`
	State             State      `json:"state"`
	Port              int        `json:"port,omitempty"`
	StartedAt         *time.Time `json:"started_at,omitempty"`
	LastErr           string     `json:"last_err,omitempty"`

	// Token is the gateway-minted token for this preview's public URL.
	// Empty when the manager's GWClient is disabled (laptop dev).
	Token string `json:"token,omitempty"`

	// URL is the public preview URL — preview-<Token>.<root>. Empty
	// when Token is empty. Clients pass this verbatim to whatever
	// renders the bundle.
	URL string `json:"url,omitempty"`

	// ExpiresAt is when the gateway will stop honoring the token.
	// Re-register (re-call /preview/start) before this to refresh.
	ExpiresAt *time.Time `json:"expires_at,omitempty"`
}

Status is the wire-format snapshot a status endpoint returns. Exported so the mux package can encode it without cross-cutting imports.

StartedAt is a pointer so its absence (server not running) renders as JSON null instead of the time.Time zero value, which JSON-encodes as the misleading "0001-01-01T00:00:00Z".

Token/URL/ExpiresAt are populated after the gateway registers the route — when the sprite runs without a gateway integration (laptop dev), they stay empty and clients fall back to status-only display.

Jump to

Keyboard shortcuts

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