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
- Variables
- type GWClient
- type Kind
- type Manager
- func (m *Manager) LogTail(worktreeID, workDir string) []byte
- func (m *Manager) LogTailNamed(worktreeID, serviceName string) []byte
- func (m *Manager) Shutdown()
- func (m *Manager) Start(ctx context.Context, worktreeID, workDir, launchName string) (Status, error)
- func (m *Manager) Status(ctx context.Context, worktreeID, workDir string) (Status, error)
- func (m *Manager) StatusNamed(_ context.Context, worktreeID, workDir, launchName string) (Status, error)
- func (m *Manager) Stop(worktreeID string) error
- func (m *Manager) StopService(worktreeID, serviceName string) error
- type Options
- type ReadyProbe
- type RegisterRequest
- type RegisterResponse
- type RevokeRequest
- type SetupRequiredError
- type Spec
- type State
- type Status
Constants ¶
const ( DefaultIdleTimeout = 15 * time.Minute DefaultStopGrace = 3 * time.Second )
Default lifecycle timers. Bumpable via Options for tests.
Variables ¶
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") )
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 ¶
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 ¶
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.
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.
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 (*Manager) LogTail ¶
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 ¶
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) 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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
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.