- module installs move to the newest compatible registry release with one go get; submodules fetch tags, check out the release tag and require it in go.mod; go.work overrides and version pins are refreshed - unlisted submodules (git URL installs) update from their own tags and plugin.yaml; --offline restricts to that - without an ID every plugin that resolves is updated; in-app, unlisted and incompatible ones are reported; failure only when a resolved update cannot be carried out - same prompt rule as plugin:install
746 lines
28 KiB
Go
746 lines
28 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"git.golem15.com/golem15/summercms/internal/build"
|
|
"git.golem15.com/golem15/summercms/internal/dev"
|
|
"git.golem15.com/golem15/summercms/modules/bonfire"
|
|
"git.golem15.com/golem15/summercms/modules/compass"
|
|
"git.golem15.com/golem15/summercms/modules/diary"
|
|
"git.golem15.com/golem15/summercms/modules/kiosk"
|
|
"git.golem15.com/golem15/summercms/modules/lagoon"
|
|
)
|
|
|
|
func main() {
|
|
args := os.Args[1:]
|
|
g := bonfire.PeekGlobals(args)
|
|
diary.Bootstrap(os.Stderr, diary.FromGlobals(g))
|
|
root, err := bonfire.NewRoot("summer", toolCommands(), os.Stdout,
|
|
bonfire.WithVersion(bonfire.BuildVersion()), bonfire.OnGlobals(diary.ApplyGlobals))
|
|
if err != nil {
|
|
os.Exit(bonfire.Fail(os.Stderr, "summer", err, g))
|
|
}
|
|
os.Exit(bonfire.Execute(context.Background(), root, args))
|
|
}
|
|
|
|
func toolCommands() []bonfire.Command {
|
|
return append([]bonfire.Command{
|
|
buildCommand(),
|
|
makePluginCommand(),
|
|
makeModelCommand(),
|
|
makeMigrationCommand(),
|
|
makeCommandCommand(),
|
|
makeJobCommand(),
|
|
makeAdminControllerCommand(),
|
|
addPluginCommand(),
|
|
pluginManifestCommand(),
|
|
pluginInstallCommand(),
|
|
pluginUpdateCommand(),
|
|
newCommand(),
|
|
devCommand(),
|
|
parityProxyCommand(),
|
|
parityRecordCommand(),
|
|
parityReplayCommand(),
|
|
parityBroadcastsCommand(),
|
|
parityUpstreamCommand(),
|
|
docsBuildCommand(),
|
|
docsSyncCommand(),
|
|
docsServeCommand(),
|
|
}, delegateCommands()...)
|
|
}
|
|
|
|
func buildCommand() bonfire.Command {
|
|
return bonfire.Command{
|
|
Name: "build",
|
|
Description: "Generate plugin imports and build the app binary",
|
|
Long: `Reads summer.yaml of the application in the current directory (or the nearest
|
|
one above it), generates plugins.gen.go and main.go from it, and runs go build
|
|
into bin/<binary>, passing the manifest's build.tags as -tags. On a terminal a
|
|
spinner runs while go build works. It ends with "Built bin/<binary> in <time>";
|
|
a compile error is printed and summer exits 1. Do not edit the generated
|
|
files: every build rewrites them.`,
|
|
Examples: []bonfire.Example{
|
|
{Command: "build", Description: "Regenerate main.go and build bin/<binary>"},
|
|
{Command: "build -q", Description: "Build and print only errors"},
|
|
},
|
|
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var log bytes.Buffer
|
|
run := func() error { return build.App(ctx, dir, &log) }
|
|
if bonfire.IsTerminal(out) {
|
|
err = out.Spinner("Building", run)
|
|
} else {
|
|
err = run()
|
|
}
|
|
for _, line := range strings.SplitAfter(log.String(), "\n") {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
if rest, ok := strings.CutPrefix(line, "warning: "); ok {
|
|
out.Warning(strings.TrimSuffix(rest, "\n"))
|
|
continue
|
|
}
|
|
if rest, ok := strings.CutPrefix(line, "built "); ok && err == nil {
|
|
out.Success("Built bin/" + strings.TrimSuffix(rest, "\n"))
|
|
continue
|
|
}
|
|
out.Printf("%s", line)
|
|
}
|
|
return err
|
|
},
|
|
}
|
|
}
|
|
|
|
func makePluginCommand() bonfire.Command {
|
|
return bonfire.Command{
|
|
Name: "make:plugin",
|
|
Description: "Scaffold a compiling plugin module from a vendor.plugin id",
|
|
Long: `Creates plugins/<vendor>/<plugin> in the current application: a Go module with plugin.go
|
|
(the plugin ID, Register and Boot) and go.mod, with a replace for a local
|
|
framework checkout when the application uses one. Register it with plugin:add
|
|
so the next summer build compiles it in. Run it inside an application; the
|
|
directory must not exist yet.`,
|
|
Examples: []bonfire.Example{
|
|
{Command: "make:plugin acme.blog", Description: "Create plugins/acme/blog with the ID acme.blog"},
|
|
{Command: "plugin:add plugins/acme/blog", Description: "Then register it in summer.yaml, go.mod and go.work"},
|
|
},
|
|
Args: []bonfire.Arg{{
|
|
Name: "id",
|
|
Description: "Plugin ID in vendor.plugin form",
|
|
Required: true,
|
|
}},
|
|
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
|
id, ok := in.Argument("id")
|
|
if !ok || id == "" {
|
|
return fmt.Errorf("make:plugin requires a vendor.plugin id")
|
|
}
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
created, err := build.MakePlugin(ctx, dir, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out.Success("Created " + created)
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func makeModelCommand() bonfire.Command {
|
|
return bonfire.Command{
|
|
Name: "make:model",
|
|
Description: "Generate a GORM model and create-table migration",
|
|
Long: `Writes models/<name>.go with a GORM model and, unless --no-migration, a
|
|
create-table migration updates/<timestamp>_create_<table>.go, in the plugin
|
|
with this ID (or the plugin whose directory you are in, when the ID is left
|
|
out). The table is the plugin ID and the plural name in snake case, such as
|
|
acme_blog_posts. The plugin's registry.gen.go lists the new files, so the
|
|
plugin picks them up on the next build. Existing files and names are never
|
|
overwritten.`,
|
|
Examples: []bonfire.Example{
|
|
{Command: "make:model acme.blog Post", Description: "A Post model and its create-table migration"},
|
|
{Command: "make:model Post --no-migration", Description: "Inside plugins/acme/blog: only the model"},
|
|
},
|
|
Args: makeArtifactArgs(),
|
|
Flags: []bonfire.Flag{{
|
|
Name: "no-migration",
|
|
Description: "Omit the create-table migration",
|
|
Bare: true,
|
|
}},
|
|
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
id, name, err := resolveMakeArgs(in, "make:model")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
result, err := build.MakeModel(ctx, dir, id, name, flagTrue(in, "no-migration"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return printArtifact(out, result)
|
|
},
|
|
}
|
|
}
|
|
|
|
func makeMigrationCommand() bonfire.Command {
|
|
return bonfire.Command{
|
|
Name: "make:migration",
|
|
Description: "Generate an ordered gormigrate stub",
|
|
Long: `Writes updates/<timestamp>_<name>.go with an empty gormigrate migration (up
|
|
and down steps to fill in) in the plugin with this ID or the plugin whose
|
|
directory you are in. The 14-digit timestamp keeps migrations in the order you
|
|
created them. registry.gen.go adds it to the plugin's migrations; run migrate
|
|
to apply it.`,
|
|
Examples: []bonfire.Example{
|
|
{Command: "make:migration acme.blog AddSlugToPosts", Description: "An empty migration in acme.blog"},
|
|
{Command: "make:migration AddSlugToPosts", Description: "The same from inside plugins/acme/blog"},
|
|
},
|
|
Args: makeArtifactArgs(),
|
|
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
id, name, err := resolveMakeArgs(in, "make:migration")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
result, err := build.MakeMigration(ctx, dir, id, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return printArtifact(out, result)
|
|
},
|
|
}
|
|
}
|
|
|
|
func makeCommandCommand() bonfire.Command {
|
|
return bonfire.Command{
|
|
Name: "make:command",
|
|
Description: "Generate a plugin console command",
|
|
Long: `Writes console/<name>.go with a bonfire.Command named <plugin>:<name> in the
|
|
plugin with this ID or the plugin whose directory you are in; the generated
|
|
registry adds it to the commands the plugin returns. The generated function
|
|
receives the application the plugin kept from Boot (*backpack.App), which
|
|
gives it the config and the services plugins publish. A function with another
|
|
parameter, such as the porting guide's withDB for blog:publish, is returned
|
|
from the plugin's Commands by hand.`,
|
|
Examples: []bonfire.Example{
|
|
{Command: "make:command acme.blog Publish", Description: "A blog:publish command in acme.blog"},
|
|
{Command: "make:command Publish", Description: "The same from inside plugins/acme/blog"},
|
|
},
|
|
Args: makeArtifactArgs(),
|
|
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
id, name, err := resolveMakeArgs(in, "make:command")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
result, err := build.MakeCommand(ctx, dir, id, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return printArtifact(out, result)
|
|
},
|
|
}
|
|
}
|
|
|
|
func makeJobCommand() bonfire.Command {
|
|
return bonfire.Command{
|
|
Name: "make:job",
|
|
Description: "Generate a plugin job without importing River",
|
|
Long: `Writes jobs/<name>.go with a queued job built with conga.Job (no River import)
|
|
in the plugin with this ID or the plugin whose directory you are in; the
|
|
generated registry adds it to the jobs the plugin returns. Dispatch it with
|
|
conga.Manager.Dispatch; queue:work or serve runs it.`,
|
|
Examples: []bonfire.Example{
|
|
{Command: "make:job acme.blog SendDigest", Description: "A SendDigest job in acme.blog"},
|
|
{Command: "make:job SendDigest", Description: "The same from inside plugins/acme/blog"},
|
|
},
|
|
Args: makeArtifactArgs(),
|
|
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
id, name, err := resolveMakeArgs(in, "make:job")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
result, err := build.MakeJob(ctx, dir, id, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return printArtifact(out, result)
|
|
},
|
|
}
|
|
}
|
|
|
|
func makeAdminControllerCommand() bonfire.Command {
|
|
return bonfire.Command{
|
|
Name: "make:admin-controller",
|
|
Description: "Generate an admin controller with fields.yaml and columns.yaml",
|
|
Long: `Writes controllers/<name>.go, its config_form.yaml and config_list.yaml, and
|
|
starter fields.yaml and columns.yaml under models/<model>/, in the plugin with
|
|
this ID or the plugin whose directory you are in. The model is the singular of
|
|
the name (Posts edits Post) unless --model names it. When models/ already has
|
|
that model, NewRecord returns it; otherwise NewRecord returns nil until you run
|
|
make:model and return it, so the screens stay closed until you finish it. The
|
|
list offers create and delete. The controller requires the permission
|
|
<plugin>.access_<name>, which you declare in the plugin's Permissions(). A
|
|
scaffolded plugin serves the YAML through AdminFS from registry.gen.go.`,
|
|
Examples: []bonfire.Example{
|
|
{Command: "make:admin-controller acme.blog Posts", Description: "A Posts controller for the Post model in acme.blog"},
|
|
{Command: "make:admin-controller Posts", Description: "The same from inside plugins/acme/blog"},
|
|
{Command: "make:admin-controller acme.blog People --model Person", Description: "A People controller for the Person model"},
|
|
},
|
|
Args: makeArtifactArgs(),
|
|
Flags: []bonfire.Flag{{
|
|
Name: "model",
|
|
Description: "The model the controller edits (default: the singular of the name)",
|
|
}},
|
|
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
id, name, err := resolveMakeArgs(in, "make:admin-controller")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
model, _ := in.Flag("model")
|
|
result, err := build.MakeAdminController(ctx, dir, id, name, model)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return printArtifact(out, result)
|
|
},
|
|
}
|
|
}
|
|
|
|
func makeArtifactArgs() []bonfire.Arg {
|
|
return []bonfire.Arg{
|
|
{Name: "plugin", Description: "Plugin ID in vendor.plugin form, or the artifact name when already inside the plugin"},
|
|
{Name: "name", Description: "Exported Go identifier"},
|
|
}
|
|
}
|
|
|
|
func resolveMakeArgs(in bonfire.Input, cmd string) (id, name string, err error) {
|
|
args := in.Args()
|
|
switch len(args) {
|
|
case 2:
|
|
return args[0], args[1], nil
|
|
case 1:
|
|
return "", args[0], nil
|
|
default:
|
|
return "", "", fmt.Errorf("%s requires <vendor.plugin> <Name>, or <Name> inside a plugin directory", cmd)
|
|
}
|
|
}
|
|
|
|
func flagTrue(in bonfire.Input, name string) bool {
|
|
v, ok := in.Flag(name)
|
|
if !ok {
|
|
return false
|
|
}
|
|
switch strings.ToLower(strings.TrimSpace(v)) {
|
|
case "", "true", "1", "yes":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func printArtifact(out bonfire.Output, result build.ArtifactResult) error {
|
|
for _, path := range result.Files {
|
|
out.Success("Created " + path)
|
|
}
|
|
if result.Hint != "" {
|
|
out.Printf("%s\n", result.Hint)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func devCommand() bonfire.Command {
|
|
return bonfire.Command{
|
|
Name: "dev",
|
|
Description: "Watch sources and rebuild the app binary",
|
|
Long: `Builds the application in the current directory, runs bin/<binary> serve and
|
|
watches the application's Go, YAML, go.mod, go.work, summer.yaml and .env
|
|
files (bin/, tmp/, .git/, main.go and plugins.gen.go excluded). After a change
|
|
it rebuilds and restarts serve; a failed build keeps the running server.
|
|
|
|
The global options summer dev was given (-v, -q, --ansi, --no-ansi, -n,
|
|
--env) are passed on to serve. Ctrl+C stops serve, which prints its own
|
|
shutdown report, then prints the dev report (rebuilds, failed builds,
|
|
restarts, uptime) and exits 0.`,
|
|
Examples: []bonfire.Example{
|
|
{Command: "dev", Description: "Build, serve and rebuild on every change"},
|
|
{Command: "dev -v", Description: "Pass -v on to serve: per-plugin summary lines and info logs of every component"},
|
|
},
|
|
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
|
|
defer stop()
|
|
return dev.Watch(ctx, dir, out, bonfire.GlobalsOf(out).Args())
|
|
},
|
|
}
|
|
}
|
|
|
|
func addPluginCommand() bonfire.Command {
|
|
return bonfire.Command{
|
|
Name: "plugin:add",
|
|
Description: "Register a local plugin module in the app manifest and workspace",
|
|
Long: `Adds a plugin that already exists on disk (a directory with plugin.go and
|
|
go.mod, for example one made by make:plugin or a shared plugin checked out as
|
|
a git submodule) to the application: its ID and module go into summer.yaml, a
|
|
require and a local replace into the application's go.mod, and the directory
|
|
into go.work. Run summer build afterwards. A plugin ID or module that is
|
|
already registered is an error.`,
|
|
Examples: []bonfire.Example{
|
|
{Command: "plugin:add plugins/acme/blog", Description: "Register the plugin in plugins/acme/blog"},
|
|
},
|
|
Args: []bonfire.Arg{{
|
|
Name: "dir",
|
|
Description: "Local plugin directory",
|
|
Required: true,
|
|
}},
|
|
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
|
pluginDir, ok := in.Argument("dir")
|
|
if !ok || pluginDir == "" {
|
|
return fmt.Errorf("plugin:add requires a local plugin directory")
|
|
}
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := build.AddPlugin(ctx, dir, pluginDir); err != nil {
|
|
return err
|
|
}
|
|
out.Success("Added " + pluginDir + " to summer.yaml, go.mod and go.work; run summer build")
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
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
|
|
},
|
|
}
|
|
}
|
|
|
|
// registryFor opens the application's config and returns the registry
|
|
// client plugins.registry_url names, or nil when the setting is empty or
|
|
// the run is --offline.
|
|
func registryFor(appDir string, offline bool) (build.Registry, error) {
|
|
if offline {
|
|
return nil, nil
|
|
}
|
|
cfg, err := compass.Open(compass.Options{Dir: filepath.Join(appDir, "config")})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
base := kiosk.RegistryURL(cfg)
|
|
if base == "" {
|
|
return nil, nil
|
|
}
|
|
client, err := kiosk.NewClient(base, cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return client, nil
|
|
}
|
|
|
|
func pluginInstallCommand() bonfire.Command {
|
|
return bonfire.Command{
|
|
Name: "plugin:install",
|
|
Description: "Install a plugin from the registry or a git repository",
|
|
Long: `Adds a plugin the application does not have yet. The argument is a plugin ID
|
|
(vendor.name), an ID pinned to a release (vendor.name@vX.Y.Z), or a git URL
|
|
(https://, ssh:// or user@host:path).
|
|
|
|
For an ID, the registry (plugins.registry_url in the application's config,
|
|
https://summercms.io by default, SUMMER_PLUGINS__REGISTRY_URL overrides it)
|
|
lists the plugin's releases. The newest release whose framework minimum the
|
|
application's framework version meets is chosen; a pinned release must meet it
|
|
too. When only releases needing a newer framework exist, the command fails and
|
|
names the framework version to move to; it never changes the framework. The
|
|
plugin is installed as a Go module with go get and listed in summer.yaml.
|
|
--source installs it as a git submodule under plugins/<vendor>/<name> instead.
|
|
|
|
Required plugins are installed with it, in the same mode, each at the newest
|
|
compatible release that meets its minimum; one the application already has
|
|
must be at least that minimum. Submodule installs require the tag in go.mod,
|
|
add the plugin to go.work and add the go.work replaces its own go.mod needs.
|
|
--source and git URLs need the application to be a git repository.
|
|
|
|
A git URL is cloned with your own git credentials and never sent to the
|
|
registry. The newest release tag whose plugin.yaml framework minimum fits is
|
|
checked out (the default branch head, with a warning, when none fits), and
|
|
the plugin is added as a submodule. Its required plugins are looked up in the
|
|
registry; with --offline they must be installed already.
|
|
|
|
The plugins to add are printed as id@version (mode) and confirmed with a
|
|
prompt. --yes skips the prompt; a run without a terminal needs --yes. Run
|
|
summer build afterwards. --offline makes no registry call, so it works only
|
|
with a git URL. An empty plugins.registry_url disables registry calls.`,
|
|
Examples: []bonfire.Example{
|
|
{Command: "plugin:install acme.blog", Description: "Install the newest compatible release of acme.blog as a Go module"},
|
|
{Command: "plugin:install acme.blog@v0.3.0 --yes", Description: "Install release v0.3.0 without asking"},
|
|
{Command: "plugin:install acme.blog --source", Description: "Install it as a git submodule under plugins/acme/blog"},
|
|
{Command: "plugin:install git@git.example.com:acme/sm-blog-plugin.git", Description: "Install a private plugin from its repository, as a submodule"},
|
|
},
|
|
Args: []bonfire.Arg{{
|
|
Name: "plugin",
|
|
Description: "vendor.name, vendor.name@vX.Y.Z or a git URL",
|
|
Required: true,
|
|
}},
|
|
Flags: []bonfire.Flag{
|
|
{Name: "source", Description: "Install registry plugins as git submodules under plugins/<vendor>/<name>", Bare: true},
|
|
{Name: "yes", Description: "Install without asking for confirmation", Bare: true},
|
|
{Name: "offline", Description: "Make no registry call; dependencies must be installed already", Bare: true},
|
|
},
|
|
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
|
arg, ok := in.Argument("plugin")
|
|
if !ok || arg == "" {
|
|
return fmt.Errorf("plugin:install requires a plugin ID or a git URL")
|
|
}
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
appDir, err := build.FindAppDir(dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
offline := bonfire.FlagBool(in, "offline")
|
|
reg, err := registryFor(appDir, offline)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
plan, err := build.PlanInstall(ctx, build.InstallOptions{
|
|
StartDir: dir,
|
|
Arg: arg,
|
|
Source: bonfire.FlagBool(in, "source"),
|
|
Yes: bonfire.FlagBool(in, "yes"),
|
|
Offline: offline,
|
|
Registry: reg,
|
|
Interactive: bonfire.Interactive(out),
|
|
Confirm: func(q string) (bool, error) { return out.Confirm(q, false) },
|
|
Out: out,
|
|
})
|
|
if errors.Is(err, build.ErrCancelled) {
|
|
out.Println("Nothing installed.")
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return build.ApplyInstall(ctx, plan, out)
|
|
},
|
|
}
|
|
}
|
|
|
|
func pluginUpdateCommand() bonfire.Command {
|
|
return bonfire.Command{
|
|
Name: "plugin:update",
|
|
Description: "Move installed plugins to their newest compatible release",
|
|
Long: `Updates one installed plugin, or every plugin in summer.yaml when no ID is
|
|
given, to the newest release whose framework minimum the application's
|
|
framework version meets. A plugin installed as a Go module moves with go get;
|
|
one installed as a git submodule fetches its tags, is checked out at the
|
|
release tag, and go.mod requires that tag. The go.work replaces the
|
|
application needs are refreshed, including version pins that follow the
|
|
framework requirement.
|
|
|
|
Releases come from the registry (plugins.registry_url). A submodule the
|
|
registry does not list, such as a plugin installed from a git URL, is updated
|
|
from its own tags and their plugin.yaml. A newer release that needs a newer
|
|
framework is reported, not installed. Plugins that cannot be updated (in-app
|
|
plugins, module installs the registry does not list, releases needing a
|
|
plugin the application lacks) are listed as not updated; the command fails
|
|
only when an update it resolved could not be carried out.
|
|
|
|
The updates are printed and confirmed with a prompt; --yes skips it, and a run
|
|
without a terminal needs --yes. --offline makes no registry call and updates
|
|
submodules from their own tags only. Run summer build afterwards.`,
|
|
Examples: []bonfire.Example{
|
|
{Command: "plugin:update acme.blog", Description: "Move acme.blog to its newest compatible release"},
|
|
{Command: "plugin:update --yes", Description: "Update every plugin that has a newer compatible release, without asking"},
|
|
{Command: "plugin:update --offline", Description: "Update submodule plugins from their own tags, without the registry"},
|
|
},
|
|
Args: []bonfire.Arg{{
|
|
Name: "plugin",
|
|
Description: "Plugin ID in vendor.name form (default: every plugin in summer.yaml)",
|
|
}},
|
|
Flags: []bonfire.Flag{
|
|
{Name: "yes", Description: "Update without asking for confirmation", Bare: true},
|
|
{Name: "offline", Description: "Make no registry call; update submodules from their own tags", Bare: true},
|
|
},
|
|
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
|
id, _ := in.Argument("plugin")
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
appDir, err := build.FindAppDir(dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
offline := bonfire.FlagBool(in, "offline")
|
|
reg, err := registryFor(appDir, offline)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = build.Update(ctx, build.UpdateOptions{
|
|
StartDir: dir,
|
|
ID: id,
|
|
Yes: bonfire.FlagBool(in, "yes"),
|
|
Offline: offline,
|
|
Interactive: bonfire.Interactive(out),
|
|
Registry: reg,
|
|
Confirm: func(q string) (bool, error) { return out.Confirm(q, false) },
|
|
Out: out,
|
|
})
|
|
if errors.Is(err, build.ErrCancelled) {
|
|
out.Println("Nothing updated.")
|
|
return nil
|
|
}
|
|
return err
|
|
},
|
|
}
|
|
}
|
|
|
|
func newCommand() bonfire.Command {
|
|
return bonfire.Command{
|
|
Name: "new",
|
|
Description: "Create a SummerCMS application in sm-<name>-app",
|
|
Long: `Creates sm-<name>-app in the current directory: summer.yaml, go.mod and go.work
|
|
(pointing at a local framework checkout found next to it, or --framework),
|
|
config/ including config/log.yaml, .env with a fresh app.key and
|
|
.env.example, compose.yaml for PostgreSQL, a starter plugin (unless
|
|
--no-plugin) and a git repository (unless --no-git). It prints the next steps:
|
|
start the database, summer build, summer migrate and summer serve.`,
|
|
Examples: []bonfire.Example{
|
|
{Command: "new acme", Description: "Create sm-acme-app with a starter plugin"},
|
|
{Command: "new acme --no-plugin --no-git", Description: "An empty application without a git repository"},
|
|
},
|
|
Args: []bonfire.Arg{{
|
|
Name: "name",
|
|
Description: "Application name: lower-case letters and digits, starting with a letter; not a Go keyword, main or internal",
|
|
Required: true,
|
|
}},
|
|
Flags: []bonfire.Flag{
|
|
{Name: "framework", Description: "Path to the framework checkout (default: a sibling summercms or summercms.go checkout)"},
|
|
{Name: "no-plugin", Description: "Do not create the starter plugin", Bare: true},
|
|
{Name: "no-git", Description: "Do not initialise a git repository", Bare: true},
|
|
},
|
|
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
|
name, ok := in.Argument("name")
|
|
if !ok || name == "" {
|
|
return fmt.Errorf("new requires an application name")
|
|
}
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
framework, _ := in.Flag("framework")
|
|
key, err := lagoon.GenerateAppKey()
|
|
if err != nil {
|
|
return fmt.Errorf("new: generate application key: %w", err)
|
|
}
|
|
_, err = build.NewApp(ctx, build.NewOptions{
|
|
Name: name,
|
|
Parent: dir,
|
|
Framework: framework,
|
|
NoPlugin: flagTrue(in, "no-plugin"),
|
|
AppKey: key,
|
|
NoGit: flagTrue(in, "no-git"),
|
|
}, out)
|
|
return err
|
|
},
|
|
}
|
|
}
|