Documentation
¶
Overview ¶
Package phpscript is the plugin boundary: a module is a directory with a modinfo.php in it, run by the phpscript interpreter the binary holds, served at /m/<vendor>/<name> as the body of an ordinary task-ui page.
A Manager finds the modules under its roots, runs each modinfo.php once to learn what it registers, and keeps a pool of runtimes per module to serve requests from. Four host symbols reach the PHP: the Task\Module constructor a modinfo.php builds, Task::register, which records it, Task::migrate, which applies the module's own schema, and Task::dispatch, which starts a Claude run. Which module is calling is not an argument of any of them; it is on the runtime's context, so the four bindings are the same for every module.
The handler rewrites nothing. $_SERVER["REQUEST_URI"] holds the path the browser requested, the /m/<vendor>/<name> prefix included, and a module builds its links from what it reads. Writing one is docs/modules.md.
Index ¶
- Constants
- Variables
- func ArtifactHref(folder string) string
- func Bind(rt *runner.Runtime, opts Options, registered func(*Module))
- func Inside(root, path string) (string, error)
- func IsImage(rel string) bool
- func Observe(rt *runner.Runtime, tracer *oida.Tracer)
- func ParseName(name string) (string, string, error)
- func Route(urlPath string) (string, string, error)
- func Startups(root string) ([]string, error)
- func WithModule(ctx context.Context, m *Module) context.Context
- type Database
- type Dispatcher
- type Found
- type Manager
- func (m *Manager) Module(name string) (*Module, bool)
- func (m *Manager) Modules() []Module
- func (m *Manager) Reload(ctx context.Context) error
- func (m *Manager) Render(ctx context.Context, w http.ResponseWriter, r *http.Request) (Result, error)
- func (m *Manager) Start(ctx context.Context) error
- func (m *Manager) Timeout() time.Duration
- type Migrator
- type Module
- func (m *Module) Href() string
- func (m *Module) Label() string
- func (m *Module) Name() string
- func (m *Module) Project() string
- func (m *Module) SetDescription(description string)
- func (m *Module) SetIcon(icon string)
- func (m *Module) SetName(name string) error
- func (m *Module) SetTitle(title string)
- func (m *Module) SetVersion(version string)
- func (m *Module) Validate() error
- type Options
- type Registry
- type Result
- type Runs
- type Schema
- type Store
- type Tree
Constants ¶
const ( Public = "public" Index = "index.php" )
Public is the directory of a module served as bytes, and Index is what answers every path it does not hold.
const ArtifactPrefix = "/artifacts/files/"
ArtifactPrefix is where a relative link in a module's markdown resolves: the instance's artifact tree, which is where a run wrote the files its report names.
const Connection = "task-ui"
Connection is the name a module opens the instance's database under: `new Database("task-ui")`. There is one, and every module shares it, because a module's tables are task-ui's tables in task-ui's database.
const DefaultTimeout = 30 * time.Second
DefaultTimeout bounds one render. phpscript holds a TimeLimit option and enforces none of it, so the bound is kept here: a module that loops forever ends with the page and leaves the instance alone.
const ModInfo = "modinfo.php"
ModInfo is the file a module is found by.
const ModuleRule = "module"
ModuleRule is the permission a module's sidebar row needs. Every module shares it, so granting a role the modules grants every module: a plugin is installed by putting its directory under a root, and a rule per module would be a rule nobody wrote down.
const Prefix = "/m"
Prefix is where every module is served from. A path under it names the module in its first two segments and belongs to the module in the rest.
const ProjectLimit = 16
ProjectLimit is how long a migration project name may be: the column mig records one in holds sixteen characters.
const ReadLimit = 256 << 10
ReadLimit is the most a module reads out of one artifact. A report is prose and a schema is a few kilobytes; anything past this is a file to link rather than a file to render, and the page links it either way.
Variables ¶
var ErrEscapes = errors.New("the path leaves the module root")
ErrEscapes is returned when a module names a path outside its own root.
var ErrName = errors.New("a module name is vendor/plugin")
ErrName is returned when a module's name is not <vendor>/<plugin>.
var ErrNoModule = errors.New("no module on the context")
ErrNoModule is returned when a binding runs on a runtime the Manager did not set a module on.
var ErrNotFound = errors.New("no module there")
ErrNotFound is returned when the path names no module, or a path under a module that its public tree does not hold and that index.php answered 404 for.
Functions ¶
func ArtifactHref ¶
ArtifactHref is the link a path inside one folder of the tree reads as, the whole tree when the folder is empty. A report written inside a folder names its files relative to that folder, and the page has to put the folder back.
func Bind ¶
Bind registers the host symbols of a module's runtime: the `Task\Module` constructor, and `Task::register`, `Task::migrate` and `Task::dispatch` as host functions whose names hold `::`. registered takes what a modinfo.php registers, and nil refuses the call.
The context.Context each binding takes first is the interpreter's and names the module; PHP counts it as no argument. docs/modules.md is the contract and docs/todo-correction.md says why the constructor is not `Task::Module`.
func Inside ¶
Inside returns path as a path relative to root, and refuses one that leaves it. A module reads its own directory: a `..` segment, an absolute path and a symlink pointing out all come back as an error.
func IsImage ¶
IsImage reports whether a path is one the page embeds rather than links, by its extension: a schema diagram is read on the page and a SQL file is a link.
func Observe ¶
Observe points a runtime's telemetry at the instance's own tracer, so one trace in /debug/oida holds the request and the PHP that ran under it. phpscript's telemetry package aliases the same oida, so the tracer passes through unwrapped. A nil tracer records nothing.
func ParseName ¶
ParseName splits a module's name into its vendor and its plugin.
Both halves are lower case, and each one holds letters, digits, a dash, an underscore or a dot: the name reaches a URL, a table row and a directory, and these are the characters all three read the same. A name with a slash too few or too many, an empty half or a dot segment is refused.
func Route ¶
Route splits a request path into the module's name and what is left for the module to answer. A path with fewer than two segments after the prefix names no module.
func Startups ¶
Startups returns the module's other @startup files, by path, without its modinfo.php: that one runs first and on its own, because nothing else may run before a module has registered.
A module puts its migrations in one of these rather than requiring the file from modinfo.php, so a startup step is a file with an annotation and the list of them is the directory.
Types ¶
type Database ¶
Database is what resolves the connection a module's PHP opens. It answers one name with the instance's own pool and refuses the rest, so a module reaches the database it migrated into and no other.
The pool is the instance's: sqlite on one connection. A module holding a transaction open holds the instance's only connection with it, which is why a module's work belongs in a statement and not in a long transaction.
func NewDatabase ¶
NewDatabase returns the provider over a pool. A nil pool answers every name with an error, which is an instance whose modules store nothing.
type Dispatcher ¶
type Dispatcher interface {
Dispatch(ctx context.Context, m *Module, comment, action, folder string) (string, error)
}
Dispatcher starts a Claude run for a module. `Task::dispatch` reaches it with the comment a user wrote and the path of a markdown file under the module's root; the prompt is that file, then the comment, then the run's own environment, the artifact tree among it.
The folder is where in the artifact tree the run writes, and the only part of the tree the prompt names. A module that gives each of its threads a folder passes it here and the run gets that folder to itself; empty is the top of the tree, which every run shares.
It returns the identifier of what it started, which the module records beside the comment, so a page links the run that answered it.
type Found ¶
Found is a modinfo.php a walk reached, and the directory holding it, which is the module's root. Startup reports whether the file holds the @startup annotation; a file without it is not run, and the Manager says so.
func Discover ¶
Discover walks each root for a modinfo.php and returns what it found, by path, so two starts register the modules in the same order.
A root that is not there is not an error: an instance with no modules directory yet has no modules, which is the state of a fresh data folder before the embedded tree is extracted. A root it cannot read is.
vendor, node_modules and .git are never descended into, so a composer dependency cannot publish a module of its own into the instance.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager holds the modules of the instance: what Discover found, what each modinfo.php registered, and a pool of runtimes per module to serve requests from. One Manager serves the process.
A nil Manager answers as an instance with no modules, so a handler needs no check of its own.
func (*Manager) Reload ¶
Reload drops every module and reads the roots again, so a module added to a checkout appears without a restart.
func (*Manager) Render ¶
func (m *Manager) Render(ctx context.Context, w http.ResponseWriter, r *http.Request) (Result, error)
Render answers one request against a module: a file under its public directory as bytes, and anything else as whatever index.php echoed.
The request is not rewritten, so $_SERVER["REQUEST_URI"] holds the path the browser requested, prefix and all.
func (*Manager) Start ¶
Start walks the roots, runs every modinfo.php once and records what each registered. It is the whole of a module's registration: nothing else runs a modinfo.php, and a module that did not register here does not serve.
A module that throws, registers nothing or names itself wrongly is reported through OnError and left out; the modules that did register serve. A modinfo.php the server cannot read is the one failure that comes back, because an instance that cannot read a file it found is misconfigured rather than empty. A root that is not there, or that holds no module, is neither.
type Migrator ¶
Migrator applies a module's own schema into task-ui's database. The Manager holds one and `Task::migrate` reaches it, so the SQL a module ships is applied by the same mig the instance's own migrations are.
dir is relative to the module's root, and a path leaving the root is refused: a module migrates its own schema and no other.
type Module ¶
type Module struct {
// Vendor and Plugin are the two halves of the name, as SetName split it.
Vendor string
Plugin string
// Root is the directory holding the modinfo.php, absolute.
Root string
// Title, Description and Icon are the sidebar row and the module's page
// heading. Icon names a symbol of the sprite without the i- prefix.
Title string
Description string
Icon string
// Version is whatever the module calls its own, for the module table.
Version string
}
Module is what a modinfo.php registers: the name that decides its link and its row in the module table, the directory holding it, and what the sidebar shows. A PHP script builds one with `new Task\Module` and fills it through the setters, so every field has one.
Root is not a setter. The Manager sets it to the directory the modinfo.php sits in, so a module cannot claim a directory it does not live in.
func ModuleFrom ¶
ModuleFrom returns the module of the context, and whether one is there. A context without one is a binding the Manager did not set up, which it refuses.
func NewModule ¶
NewModule returns the empty module `new Task\Module` builds, which names itself through the setters. The context is the interpreter's.
func (*Module) Label ¶
Label is the title, falling back to the plugin half of the name, so a module that set none still reads as something in the sidebar.
func (*Module) Project ¶
Project is the module's name as mig records an applied migration under. Each module migrates under its own, so two modules cannot claim one applied row and a module added later starts from its own first migration rather than from wherever the instance stands.
A full name does not fit: `titpetric/refinement` is twenty characters and the column holds sixteen. The name is the plugin cut to seven characters, then eight hex of the sha256 of the whole name, so `titpetric/refinement` reads as `refinem_` and its digest. The prefix says which module a row belongs to at a glance; the digest is what keeps two vendors' modules of one name apart.
func (*Module) SetDescription ¶
SetDescription sets the line under the heading.
func (*Module) SetName ¶
SetName splits <vendor>/<plugin> into the two halves and refuses anything else. It is the one setter that can fail, so a modinfo.php that names itself wrongly throws where it wrote the name.
func (*Module) SetVersion ¶
SetVersion sets what the module calls its own version.
type Options ¶
type Options struct {
// Roots are the directories walked for a modinfo.php, in order. The first
// is the instance's own modules directory, where the embedded tree is
// extracted; a workspace root after it reaches a module in a checkout.
Roots []string
// Registry records what a modinfo.php registered. Nil runs the modules
// and keeps them in memory alone, as a test does.
Registry Registry
// Migrator applies a module's schema. Nil makes Task::migrate refuse.
Migrator Migrator
// Dispatcher starts the Claude runs. Nil makes Task::dispatch refuse.
Dispatcher Dispatcher
// Runs answers which run a dispatch turned into, for the status
// label a module draws. Nil answers "" for every note.
Runs Runs
// Database resolves the connection a module opens, `new Database("task-ui")`.
// Nil leaves a module with no database.
Database *Database
// Artifacts is the instance's artifact tree as a module reads it.
// Nil leaves Task::artifacts empty and Task::artifact blank.
Artifacts *Tree
// Tracer records the spans of the PHP into the same traces as the
// instance's own middleware. Nil records nothing.
Tracer *oida.Tracer
// PoolSize is how many registered runtimes a module keeps between
// requests. One runtime serves one goroutine at a time, so this is also
// the cheapest bound on how many renders run at once.
PoolSize int
// Timeout bounds one render.
Timeout time.Duration
// Version is the instance's version, which PHP reads as Task::VERSION.
Version string
// OnError receives what the Manager cannot report through a return: a
// modinfo.php that threw, a module that registered nothing, a template
// directory it could not seed. Nil drops them.
OnError func(error)
}
Options configures the Manager. Take NewOptions and override what the instance needs, so a field added later keeps its default.
func NewOptions ¶
NewOptions returns the options of an instance serving the modules under root.
type Registry ¶
type Registry interface {
// Register upserts a module that passed Validate. A module already in the
// table keeps its row, its identifier and its place in the sidebar, and
// takes the rest from the modinfo.php just read.
Register(ctx context.Context, m *Module) error
// Forget takes the names of every module a scan found and drops the rows
// of the others, whose directories are gone. It returns how many.
Forget(ctx context.Context, found []string) (int, error)
}
Registry is where a module lands once its modinfo.php registered it: the module table, and the sidebar row that links it.
type Result ¶
type Result struct {
// Body is what the script echoed, or the bytes of a file under public/.
Body []byte
// Status is what the script staged, 200 when it staged none.
Status int
// Header holds the headers the script staged. Content-Type is set for a
// file served from public/ and left to the layout for a rendered page.
Header http.Header
// Took is how long the render ran, which the page footer prints.
Took time.Duration
// File reports a path under the module's public/ tree, served as bytes
// with no layout around it.
File bool
}
Result is what one render produced. The body reaches the response unwritten, because a module's output is the body of a task-ui page and the layout goes around it; a handler that serves a file writes Body as it stands.
type Runs ¶
Runs answers which run a module's dispatch turned into. `Task::run` reaches it with the note identifier a dispatch returned.
A dispatch opens a note and starts the run behind it, in that order, so the run does not exist yet when the dispatch answers. The module records the note and asks for the run when it draws a page, which is later and by then there is one.
It answers "" for a note with no run yet, and for no note at all. A page draws a label either way; an empty one is a run that has not started.
type Schema ¶
Schema applies a module's own schema into task-ui's database. It is what `Task::migrate("schema/")` reaches.
The module declares the SQL and the instance applies it, through the same mig the instance's own migrations go through, so a module's tables are task-ui's tables and `mig docs` reads them beside the rest.
Each module migrates under a project of its own name, so two modules cannot claim one applied row and a module added later starts from its own first migration rather than from wherever the instance stands.
type Store ¶
type Store struct {
Modules *storage.ModuleStorage
Sidebar *storage.SidebarStorage
}
Store is the Registry over the database: a module lands in the module table and gets a row in the sidebar, and a module a scan no longer finds loses both.
func NewStore ¶
func NewStore(modules *storage.ModuleStorage, sidebar *storage.SidebarStorage) *Store
NewStore returns the Registry over the two tables.
func (*Store) Forget ¶
Forget drops the module rows and the sidebar rows of every module the scan did not find, and returns how many rows went.
func (*Store) Register ¶
Register records a module and the sidebar row that links it.
The row keeps its position and its hidden flag, so a module registering on every start does not walk to the end of the sidebar, and one the user hid stays hidden. The title and the icon are the modinfo.php's, so editing that file and restarting is how a module renames its own row.
type Tree ¶
type Tree struct {
Store *artifactstore.Store
}
Tree is the instance's artifact tree as a module reads it: the files its runs left, and the text of one.
A module cannot reach the tree on its own. The filesystem boundary roots every path a script opens at the module's own directory, which is the point: a plugin reads its own files and the artifacts through a binding that knows what the artifacts are.
Reading only. A module shows what a run wrote; the runs write.
func NewTree ¶
func NewTree(store *artifactstore.Store) *Tree
NewTree returns the reader over the instance's artifact tree. A nil store is an instance with none, and both calls answer empty.
func (*Tree) Files ¶
Files is every file under one folder of the tree, by path relative to that folder, as a module lists them. An empty folder is the whole tree. Directories are walked and not listed: a module shows files.
The folder is how a module keeps its own scratch space apart from the instance's: a plugin that gives each of its threads a folder lists one thread's files by naming it, and the paths come back the way the run wrote them, without the folder in front.