Files
summercms/cmd/summer/main.go
Jakub Zych d024ab4bb8 feat(build): summer plugin:update
- 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
2026-10-29 18:46:14 +01:00

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
},
}
}