Plugins

Source fetched from git, pinned and compiled in: using one, and writing one

# Plugins

A plugin is a git repository of Pascal source that an app builds with, the
way it builds with the framework: fetched, pinned to a commit, and
compiled into the one binary. Nothing is loaded at run time.

```sh
askr plugin add https://github.com/kwhorne/askr-stripe.git
askr build
```

```toml
# askr.toml, as plugin add writes it
[plugins.stripe]
git = "https://github.com/kwhorne/askr-stripe.git"
version = "^0.1.0"
```

| | |
|---|---|
| `askr plugin add <git url>` | Fetch the newest release, pin it, and wire it into app.lpr |
| `askr plugin update [name]` | Move to the newest release askr.toml allows, after showing its UPGRADE.md |
| `askr plugin remove <name>` | Take it out of askr.toml and askr.lock |
| `askr plugin list` | What this project builds with |
| `askr install` | Fetch the framework and every plugin the lock names |
| `askr outdated` | What is published, for the framework and each plugin |

## Why source, and why git

Pascal compiles into one binary, and a `.ppu` file is bound to one exact
compiler version, so there is nothing a plugin could be but source. It
goes where the framework goes -- `~/.askr/pkg/plugins/<name>@<version>` --
and `askr.lock` records the commit it stands on:

```toml
[plugins.stripe]
git = "https://github.com/kwhorne/askr-stripe.git"
version = "0.1.2"
commit = "9f3c…"
```

There is no registry. The version lives in the tag, the commit in the
lock, and a registry would be a third place that can disagree with both.
A version is a tag like `v0.1.2`, and the range in askr.toml follows
npm's rules, zero major included: `^0.1.0` allows 0.1.x and not 0.2.0.

Working on a plugin, point at the checkout instead, as with `[askr] path`:

```toml
[plugins.stripe]
path = "../askr-stripe"
```

## There is no sandbox

A plugin is compiled into your binary and can do anything your app can:
read its configuration, its database, its keys. **The commit in askr.lock
is the whole of the trust model.** `askr install` refuses a cached copy
that stands on another commit than the lock -- a moved tag, or a touched
cache -- and says how to fetch it again. Read what you add, the way you
would read a dependency that ships as source, because that is what it is.

## What the build checks

Every build resolves the plugins askr.toml names and stops, with the
reason, when:

- a plugin builds against another Askr than this project does;
- two plugins claim the same table, or the same configuration prefix;
- askr.lock disagrees with askr.toml -- another git url, or a pin the
  locked version no longer meets -- until `askr install` or
  `askr plugin update` settles it;
- app.lpr does not start the plugins (below);
- askr.toml says `plugins.stripe = "^0.1.0"`, the form Cargo uses. A plugin
  is a section of its own here, and without this check that line would
  have meant no plugin at all.

It then writes `App.Plugins` into `.build/plugins`: a unit that uses each
plugin's entry unit and every migration it has. A unit nothing refers to
is never linked, and its initialization never runs -- the same reason
`App.Migrations` exists.

## In app.lpr

```pascal nocheck
uses
  ...
  Askr.Plugins, App.Plugins,
  App.Migrations, App.Seeders,
  ...

  UseCsrf(R);
  UseAuth(R);
  UsePlugins(R);
```

`askr new` writes both lines. `askr plugin add` adds them to an app.lpr
from before plugins, at the places `askr new` puts them; if the file does
not look that way, it changes nothing and shows the lines to add.

`UsePlugins` comes after the app's middleware, so a plugin's routes have
sessions and sign-in. Each plugin is made there, in the order they
registered: `Configure`, then `Routes`. A plugin that cannot start stops
the app, naming it and why.

## Writing a plugin

A repository with `askr-plugin.toml` at its root:

```toml
name = "stripe"
version = "0.1.0"
askr = "^0.16.0"             # the Askr versions it builds against
entry = "Askr.Plugin.Stripe" # the unit that registers it
units = "src"                # unit directories, relative to the root
migrations = "database"      # optional
docs = "docs"                # optional; the default
config = "stripe"            # its configuration prefix; the name by default
tables = "stripe_customers, stripe_events"
```

The entry unit registers the plugin, and nothing else, in its
initialization:

```pascal nocheck
unit Askr.Plugin.Stripe;

{$mode Delphi}{$H+}

interface

uses SysUtils, Askr.Core.Config, Askr.Plugins, Askr.Http.Router;

type
  TStripePlugin = class(TPlugin)
  public
    function Name: string; override;
    procedure Configure; override;
    procedure Routes(R: TRouter); override;
  end;

implementation

function TStripePlugin.Name: string;
begin
  Result := 'stripe';
end;

procedure TStripePlugin.Configure;
begin
  if Cfg('stripe.secret') = '' then
    raise Exception.Create('Set STRIPE_SECRET');
end;

procedure TStripePlugin.Routes(R: TRouter);
begin
  R.Post('/stripe/webhook', Webhooks.Receive);
end;

initialization
  RegisterPlugin(TStripePlugin);

end.
```

**The initialization runs before the app's first line** -- before its
configuration is loaded -- so read nothing there. Commands and
migrations are registered there as the app's are, with `RegisterCommand`
and `RegisterMigration`; they are read when they run.

**A migration's version carries the plugin's name**:
`stripe:20261001120000`. The row in `askr_migrations` then says whose it
is, and it cannot collide with the app's. The migrator orders by the
timestamp, so a plugin's migration takes its place among the app's.

**A route is the app's or one plugin's.** A second route with the same
method and shape -- `/stripe/:id` beside `/stripe/:slug` -- stops the app
where it is added, and names the plugin that has the first. So does a
command registered twice.

**Docs** in the plugin's `docs/` are served to coding agents through
`docs_search` and `docs_read` for the version the project pins, as
`stripe/billing.md` beside the framework's pages.

**UPGRADE.md** at the root is read the way the framework's is: `askr plugin
update` prints the sections between the two versions before it moves the
lock.

Publish a release by pushing a tag, `v0.1.0`, whose `askr-plugin.toml`
says `version = "0.1.0"`. A tag that says it is another version, or
another plugin, is refused.

## What is not here

**A registry.** Git urls only, for now; a catalogue on askrcode.com can
come later as a list of them.

**Plugins that need other plugins.** A plugin depends on Askr and nothing
else. The list is flat.

**Svelte pages in a plugin.** The mechanism leaves room for it, but no
plugin needs it yet: Stripe's checkout and customer portal are Stripe's
own pages.

**A sandbox**, as above. There is no way to give compiled Pascal less than
the process it runs in.