feat(kiosk): plugin.yaml manifest and summer plugin:manifest

- new module kiosk: Manifest, Marshal, Parse, Derive, Generate, Write, Check, StaleError
- plugin:manifest [dir] with --check, --pretend, --name, --description
- docs: kiosk README, root table row, plugins/marketplace.md, scaffolding row
This commit is contained in:
Jakub Zych
2026-10-29 16:55:17 +01:00
parent 824e3d5882
commit ec6a1955fc
14 changed files with 979 additions and 1 deletions

View File

@@ -3,9 +3,11 @@ package main
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
@@ -13,6 +15,7 @@ import (
"git.golem15.com/golem15/summercms/internal/dev"
"git.golem15.com/golem15/summercms/modules/bonfire"
"git.golem15.com/golem15/summercms/modules/diary"
"git.golem15.com/golem15/summercms/modules/kiosk"
"git.golem15.com/golem15/summercms/modules/lagoon"
)
@@ -38,6 +41,7 @@ func toolCommands() []bonfire.Command {
makeJobCommand(),
makeAdminControllerCommand(),
addPluginCommand(),
pluginManifestCommand(),
newCommand(),
devCommand(),
parityProxyCommand(),
@@ -414,6 +418,92 @@ already registered is an error.`,
}
}
func pluginManifestCommand() bonfire.Command {
return bonfire.Command{
Name: "plugin:manifest",
Description: "Generate or check a plugin's plugin.yaml from its sources",
Long: `Writes plugin.yaml into a plugin directory (the current one by default). Every
field comes from the plugin's own sources: the ID and the required plugins
from what plugin.go's ID() and Requires() return (string literals or
constants, read without running any code), the module path and the framework
and plugin version minimums from go.mod, the package import path from the
directory, and the name and description from the README's H1 and the
paragraph after it. plugin.yaml is committed next to plugin.go and never
edited by hand: rerun the command after changing any of those sources.
--name and --description replace the README text and are kept in plugin.yaml
until changed again; --name="" clears the override. A README whose H1 is the
plugin ID needs --name. --pretend prints the file without writing it.
--check writes nothing and exits 1, printing the expected content, when
plugin.yaml is missing or differs from what the command would write.`,
Examples: []bonfire.Example{
{Command: "plugin:manifest plugins/acme/blog", Description: "Write plugins/acme/blog/plugin.yaml"},
{Command: "plugin:manifest --check", Description: "Fail when the current directory's plugin.yaml is missing or stale"},
{Command: "plugin:manifest --pretend", Description: "Print the manifest without writing it"},
{Command: `plugin:manifest --name="Blog"`, Description: "Use Blog as the display name instead of the README's H1"},
},
Args: []bonfire.Arg{{
Name: "dir",
Description: "Plugin directory, the one with plugin.go (default: the current directory)",
}},
Flags: []bonfire.Flag{
{Name: "check", Description: "Write nothing; exit 1 when plugin.yaml is missing or stale", Bare: true},
{Name: "pretend", Description: "Print the generated plugin.yaml instead of writing it", Bare: true},
{Name: "name", Description: "Display name override, kept in plugin.yaml; an empty value clears it"},
{Name: "description", Description: "Description override, kept in plugin.yaml; an empty value clears it"},
},
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
dir, ok := in.Argument("dir")
if !ok || dir == "" {
dir = "."
}
var opts kiosk.DeriveOptions
if v, ok := in.Flag("name"); ok {
opts.Name = &v
}
if v, ok := in.Flag("description"); ok {
opts.Description = &v
}
if bonfire.FlagBool(in, "check") {
if opts.Name != nil || opts.Description != nil {
return fmt.Errorf("plugin:manifest --check takes no --name or --description; run plugin:manifest without --check to change them")
}
err := kiosk.Check(ctx, dir)
var stale *kiosk.StaleError
if errors.As(err, &stale) {
out.Println("Expected " + stale.Path + ":")
out.Println(strings.TrimRight(string(stale.Want), "\n"))
}
if err != nil {
return err
}
out.Success(filepath.Join(dir, kiosk.FileName) + " is up to date")
return nil
}
if bonfire.FlagBool(in, "pretend") {
data, warnings, err := kiosk.Generate(ctx, dir, opts)
for _, w := range warnings {
out.Warning(w)
}
if err != nil {
return err
}
out.Println(strings.TrimRight(string(data), "\n"))
return nil
}
path, warnings, err := kiosk.Write(ctx, dir, opts)
for _, w := range warnings {
out.Warning(w)
}
if err != nil {
return err
}
out.Success("Wrote " + path)
return nil
},
}
}
func newCommand() bonfire.Command {
return bonfire.Command{
Name: "new",