customapps

package
v0.2.2 Latest Latest
Warning

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

Go to latest
Published: Aug 11, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package customapps is builder's custom-app extension point: a way to add a screen to the builder — a tile in the SDK launcher and a route of its own — without editing builder's source.

There are two kinds of custom app, and they are the same app to everything downstream:

  • A DISCOVERED app is a directory under BUILDER_APPS_DIR holding an app.json manifest and a ui.js ES module. Nothing is compiled and nothing is imported: it is found at boot by reading the directory. This is the drop-in case, and it is the one the CLI scaffolds.

  • A COMPILED app calls Register from its own init() and is blank-imported by the host project, exactly the way a togo plugin registers a provider. It gets a *sql.DB, a *slog.Logger and a chi router of its own, so it can do real backend work. Its UI is still a ui.js module, served either from disk or from an fs.FS the app hands over.

Nothing here may take the builder down. The SDK's promise is that it stays up when the product it watches is broken, and a custom app is by definition code nobody on this side reviewed: a malformed manifest, an unreadable directory, an app whose Init returns an error — each is logged and skipped, and the boot continues with one fewer app.

Index

Constants

This section is empty.

Variables

View Source
var ErrExists = errors.New("an app with this slug already exists")

ErrExists is returned when a slug is already taken.

A sentinel rather than a formatted string, because two callers act on it differently: the CLI tells a human to pass --force, and the MCP tool tells an agent to pick another slug. It is also the backstop for a race — the MCP tool checks the live registry first, but Scaffold's stat-then-create has a gap, and the loser of that race has to be told the same thing the pre-check would have said rather than something opaque.

Functions

func Register

func Register(a App)

Register wires a compiled custom app. Call it from init().

It never panics and never returns an error, because it runs during package initialisation where there is nobody to hand a failure to. An invalid or duplicate app is dropped and reported later by Validate, which the provider logs at boot — a silent no-op would be the worst of both worlds.

func RegistrationErrors

func RegistrationErrors() []string

RegistrationErrors returns what Register rejected.

Types

type App

type App struct {
	// Manifest is required. Slug, Title.EN and a valid slug shape are checked.
	Manifest Manifest

	// Init runs once at boot, after the registry has a database and a logger.
	// Optional. An error is logged and the app is dropped — it never fails the
	// boot, because a third-party app must not be able to stop the builder.
	Init func(ctx Context) error

	// Routes mounts the app's own HTTP surface at
	// /api/builder/apps/<slug>/api/*. Optional. The router it receives is
	// already behind the session middleware, so handlers may read the caller
	// with auth.IdentityFrom(r.Context()).
	Routes func(r chi.Router)

	// UIFS serves the app's ui.js when it has no directory on disk. Optional;
	// a disk directory wins when both are present.
	UIFS fs.FS
}

App is what Register takes.

func Registered

func Registered() []App

Registered returns the compiled apps, sorted by order then slug.

type Context

type Context struct {
	// DB is the application's database. Nil when the app booted without one:
	// check it. A custom app must not CREATE TABLE here — ship migrations and
	// let the host apply them, exactly as a togo plugin does.
	DB *sql.DB

	// Log is already scoped with the app's slug.
	Log *slog.Logger

	// Dir is the app's directory on disk, or "" for a compiled app that carries
	// its UI in an embedded FS instead.
	Dir string
}

Context is what a compiled app receives at boot.

A struct rather than four arguments so a later addition does not break every app that already exists — the same reason togo hands providers a *Kernel.

type Manifest

type Manifest struct {
	Slug        string `json:"slug"`
	Title       Text   `json:"title"`
	Description Text   `json:"description,omitempty"`

	// Icon is a lucide glyph name. The SDK launcher carries a small inlined set
	// and falls back to a generic tile for anything it does not have; the web
	// route resolves the full lucide-react set.
	Icon string `json:"icon,omitempty"`

	// Color is the launcher tile fill, #rrggbb. Fixed per app rather than
	// derived, for the reason the built-in ten are fixed: a hash-derived palette
	// reshuffles the whole grid the day one app is renamed.
	Color string `json:"color,omitempty"`

	// Order sorts the launcher. Built-ins occupy 0..99; custom apps default to
	// 100 so they land after them.
	Order int `json:"order,omitempty"`

	// UI is the ES module served at /api/builder/apps/<slug>/ui.js, relative to
	// the app directory. Defaults to "ui.js".
	UI string `json:"ui,omitempty"`

	// Source is set by the registry, never by app.json: "disk" for a discovered
	// app, "compiled" for one registered from Go. Read-only to the app.
	Source string `json:"source,omitempty"`

	// HasAPI reports whether the app mounted backend routes under
	// /api/builder/apps/<slug>/api. Set by the registry.
	HasAPI bool `json:"hasApi,omitempty"`

	// Path is the app's directory, reported so an operator can find the files
	// that produced a broken tile. Empty for a compiled app with no directory.
	Path string `json:"path,omitempty"`
}

Manifest is app.json. It is also exactly what GET /api/builder/apps returns, so there is one shape to learn rather than a disk format and a wire format that drift apart.

type ScaffoldOptions

type ScaffoldOptions struct {
	// Slug is the app's identity. Everything else has a default derived from it.
	Slug string
	// Root is the apps directory. Defaults to "apps".
	Root string

	TitleEN string
	TitleAR string
	DescEN  string
	DescAR  string
	Icon    string
	Color   string
	Order   int

	// Go writes a compiled-app skeleton next to the drop-in files, for an app
	// that needs a database and real handlers. Its package still has to be
	// blank-imported by the host project — the generated file says where.
	Go bool
	// GoModule is the host project's module path, used to print the import line
	// the operator has to add. Read from go.mod when empty.
	GoModule string

	Force bool
}

ScaffoldOptions drives `togo-builder app new`.

type ScaffoldResult

type ScaffoldResult struct {
	Dir      string
	Files    []string
	Manifest Manifest
	// GoImport is the blank-import line to add to internal/plugins/local.go,
	// empty unless Go was requested.
	GoImport string
}

ScaffoldResult reports what was written.

func Scaffold

func Scaffold(o ScaffoldOptions) (ScaffoldResult, error)

Scaffold writes a working custom app.

"Working" is the bar: the generated app boots, appears in the launcher, opens to a real screen, reads and writes its own state, and mirrors in Arabic — with no further wiring. A skeleton that needs three more edits before it renders anything teaches the wrong contract.

type Service

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

Service is the registry's runtime: the scan, the HTTP surface, and the per-app state store.

func New

func New(db *sql.DB, log *slog.Logger, root string) *Service

New builds the service. root is the directory scanned for drop-in apps; it does not have to exist.

func (*Service) List

func (s *Service) List() []Manifest

List returns the live manifests in launcher order.

func (*Service) Problems

func (s *Service) Problems() []string

Problems returns what the last scan refused, one sentence per rejected app. The CLI prints it; /_health serves it.

func (*Service) Root

func (s *Service) Root() string

Root reports the directory drop-in apps are read from.

func (*Service) Routes

func (s *Service) Routes(r chi.Router)

Routes mounts the surface. The caller is expected to have wrapped it in the session middleware already — nothing here authenticates.

Compiled apps' own routers are mounted here, at Routes time, because chi cannot mount a subtree behind a path parameter. Their slugs are static segments, and chi's trie matches a static segment ahead of {slug} regardless of registration order, so this cannot shadow the generic handlers.

func (*Service) Scan

func (s *Service) Scan(ctx context.Context)

Scan discovers drop-in apps and merges them with the compiled ones.

It never returns an error for a broken app. A directory that will not parse is recorded in problems and skipped: one bad app.json must cost exactly one tile, not the boot.

type Text

type Text struct {
	EN string `json:"en"`
	AR string `json:"ar"`
}

Text is one user-facing string in both languages.

Bilingual at the contract level rather than at the renderer's: an app that ships only English is a permanent English island inside an app that mirrors, and there is no later pass that fixes it. AR falls back to EN when empty, so the requirement is visible without being a barrier to a first commit.

func (Text) Get

func (t Text) Get(lang string) string

Get returns the string for a language tag, falling back to English.

Jump to

Keyboard shortcuts

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