- build runs go build under a spinner on a terminal and ends with 'Built bin/<name> in <time>'; make:* print 'Created <path>' per file - make:*, plugin:add, new, parity:*, docs:build, docs:sync and every delegate get long help and examples; delegates note that global options pass through to the application binary - make:* failures (no summer.yaml, invalid plugin ID or name, existing artifact, unknown plugin) explain themselves and suggest a command - docs:build and docs:sync return bonfire.Reported after the problem list - TestCommandsDocumented covers the tool commands too - docs: Upgrading to 0.4, delegates and global options, make: results and hints, the checklist for a finished command
457 lines
16 KiB
Go
457 lines
16 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"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/diary"
|
|
"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(),
|
|
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, "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/<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/blog with the ID acme.blog"},
|
|
{Command: "plugin:add plugins/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/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/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
|
|
takes no arguments, so a command that needs the database or another service
|
|
takes a parameter instead and is returned from the plugin's Commands by hand
|
|
(see the porting guide's blog:publish).`,
|
|
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/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/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/<name>/, in the plugin with
|
|
this ID or the plugin whose directory you are in. The controller requires the
|
|
permission <plugin>.access_<name>, which you declare in the plugin's
|
|
Permissions(), and its NewRecord returns nil until you return your model, so
|
|
the screens stay closed until you finish it.`,
|
|
Examples: []bonfire.Example{
|
|
{Command: "make:admin-controller acme.blog Posts", Description: "A Posts controller in acme.blog"},
|
|
{Command: "make:admin-controller Posts", Description: "The same from inside plugins/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:admin-controller")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
result, err := build.MakeAdminController(ctx, dir, id, name)
|
|
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 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
|
|
},
|
|
}
|
|
}
|