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 ¶
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 ¶
New builds the service. root is the directory scanned for drop-in apps; it does not have to exist.
func (*Service) Problems ¶
Problems returns what the last scan refused, one sentence per rejected app. The CLI prints it; /_health serves it.
func (*Service) Routes ¶
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.
type Text ¶
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.