diff --git a/README.md b/README.md index a9663a6..22fceab 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ The `migrate`, `migrate:status`, `migrate:rollback`, `serve` and admin commands curl http://127.0.0.1:8080/items/1 ``` -`summer migrate`, `summer migrate:status`, `summer migrate:rollback` and `summer serve`, run from the application directory, delegate to the built binary in `bin/`. +`summer migrate`, `summer migrate:status`, `summer migrate:rollback`, `summer serve`, `summer queue:work`, `summer queue:clear`, `summer schedule:run`, `summer route:list`, `summer key:generate`, `summer admin:create` and `summer admin:reset-password`, run from the application directory, delegate to the built binary in `bin/` and pass their flags to it unchanged. ## Repository layout diff --git a/cmd/summer/main.go b/cmd/summer/main.go index defda98..f0b8c70 100644 --- a/cmd/summer/main.go +++ b/cmd/summer/main.go @@ -26,7 +26,7 @@ func main() { } func toolCommands() []bonfire.Command { - return []bonfire.Command{ + return append([]bonfire.Command{ buildCommand(), makePluginCommand(), makeModelCommand(), @@ -42,17 +42,10 @@ func toolCommands() []bonfire.Command { parityReplayCommand(), parityBroadcastsCommand(), parityUpstreamCommand(), - delegateCommand("migrate", "Run plugin migrations in the app binary"), - delegateRollbackCommand(), - delegateCommand("migrate:status", "Show per-plugin migration history in the app binary"), - delegateCommand("serve", "Run the app HTTP server"), - delegateQueueWorkCommand(), - delegateScheduleRunCommand(), - delegateCommand("queue:clear", "Clear pending queued jobs in the app binary"), docsBuildCommand(), docsSyncCommand(), docsServeCommand(), - } + }, delegateCommands()...) } func buildCommand() bonfire.Command { diff --git a/cmd/summer/main_test.go b/cmd/summer/main_test.go index 520b242..dfb4733 100644 --- a/cmd/summer/main_test.go +++ b/cmd/summer/main_test.go @@ -19,7 +19,7 @@ func TestToolCommandNames(t *testing.T) { for _, c := range toolCommands() { names = append(names, c.Name) } - for _, want := range []string{"build", "make:plugin", "make:model", "make:migration", "make:command", "make:job", "make:admin-controller", "plugin:add", "new", "dev", "migrate", "migrate:rollback", "migrate:status", "serve", "queue:work", "queue:clear", "schedule:run", "parity:broadcasts", "parity:upstream", "docs:build", "docs:sync", "docs:serve"} { + for _, want := range []string{"build", "make:plugin", "make:model", "make:migration", "make:command", "make:job", "make:admin-controller", "plugin:add", "new", "dev", "migrate", "migrate:rollback", "migrate:status", "serve", "queue:work", "queue:clear", "schedule:run", "route:list", "key:generate", "admin:create", "admin:reset-password", "parity:broadcasts", "parity:upstream", "docs:build", "docs:sync", "docs:serve"} { if !slices.Contains(names, want) { t.Fatalf("missing %s in %v", want, names) } @@ -33,7 +33,6 @@ func TestToolCommandNames(t *testing.T) { "make:command": {"[plugin] [name]"}, "make:job": {"[plugin] [name]"}, "make:admin-controller": {"[plugin] [name]"}, - "schedule:run": {"--once"}, "parity:broadcasts": {"--flow", "--step", "--ids", "127.0.0.1:8424"}, "parity:upstream": {"--listen", "--ca-dir", "--out", "--mode", "--script", "--vars", "127.0.0.1:8425"}, "docs:build": {"--out", "--src", "--root", "--base-url", "--check"}, diff --git a/cmd/summer/runtime.go b/cmd/summer/runtime.go index 25f19b9..e1a6c1b 100644 --- a/cmd/summer/runtime.go +++ b/cmd/summer/runtime.go @@ -6,7 +6,6 @@ import ( "os" "os/exec" "path/filepath" - "strconv" "strings" "git.golem15.com/golem15/summercms/internal/build" @@ -16,10 +15,44 @@ import ( // appRunner runs the app binary with argv; tests replace it. var appRunner = runAppBinary -func delegateCommand(name, description string) bonfire.Command { +// delegateCommands returns every command summer runs on the app binary. One +// table is the single source: name, base description and a hint naming the +// common flags or arguments the app binary accepts. +func delegateCommands() []bonfire.Command { + rows := []struct{ name, description, hint string }{ + {"migrate", "Run plugin migrations in the app binary", ""}, + {"migrate:rollback", "Roll back the last migration of a plugin in the app binary", "--plugin "}, + {"migrate:status", "Show per-plugin migration history in the app binary", ""}, + {"serve", "Run the app HTTP server", "--addr (default :8080)"}, + {"queue:work", "Run background job workers in the app binary", "--queue (repeatable)"}, + {"queue:clear", "Clear pending queued jobs in the app binary", `[queue] argument (default "default")`}, + {"schedule:run", "Run the scheduler in the app binary (or once for system cron)", "--once"}, + {"route:list", "List the app's routes from the app binary", ""}, + {"key:generate", "Print a fresh application key from the app binary", ""}, + {"admin:create", "Create a backend administrator in the app binary", "--email (required), --login, --role , --superuser; the password is prompted or read from stdin"}, + {"admin:reset-password", "Reset a backend administrator's password in the app binary", " (login or email); the password is prompted or read from stdin"}, + } + cmds := make([]bonfire.Command, 0, len(rows)) + for _, r := range rows { + cmds = append(cmds, delegateCommand(r.name, r.description, r.hint)) + } + return cmds +} + +// delegateCommand returns a passthrough command that runs name on the app +// binary with every argument summer was given. +func delegateCommand(name, description, hint string) bonfire.Command { + long := description + ".\n\nsummer runs bin/ " + name + + " in the application directory, building the binary first when it is missing. Every flag and argument after " + + name + ", including --help, is passed to the app binary unchanged." + if hint != "" { + long += "\n\nCommon flags: " + hint + "." + } + long += "\n\nRun summer " + name + " --help inside an application to list all of them." return bonfire.Command{ Name: name, - Description: description, + Description: description + " (flags pass to the app binary)", + Long: long, Passthrough: true, Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error { args := in.Args() @@ -34,67 +67,6 @@ func delegateCommand(name, description string) bonfire.Command { } } -func delegateRollbackCommand() bonfire.Command { - return bonfire.Command{ - Name: "migrate:rollback", - Description: "Roll back the last migration of a plugin in the app binary", - Flags: []bonfire.Flag{{ - Name: "plugin", - Description: "Plugin ID whose last migration to roll back", - }}, - Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error { - argv := []string{"migrate:rollback"} - if plugin, ok := in.Flag("plugin"); ok && plugin != "" { - argv = append(argv, "--plugin", plugin) - } - argv = append(argv, in.Args()...) - return runAppBinary(ctx, argv, out) - }, - } -} - -func delegateQueueWorkCommand() bonfire.Command { - return bonfire.Command{ - Name: "queue:work", - Description: "Run background job workers in the app binary", - Flags: []bonfire.Flag{{ - Name: "queue", - Description: "Queue to work (repeatable; default all known queues)", - Repeatable: true, - }}, - Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error { - argv := []string{"queue:work"} - for _, q := range in.Flags("queue") { - argv = append(argv, "--queue", q) - } - argv = append(argv, in.Args()...) - return runAppBinary(ctx, argv, out) - }, - } -} - -func delegateScheduleRunCommand() bonfire.Command { - return bonfire.Command{ - Name: "schedule:run", - Description: "Run the scheduler in the app binary (or once for system cron)", - Flags: []bonfire.Flag{{ - Name: "once", - Description: "Run the entries due this minute and exit (for system cron)", - Bare: true, - }}, - Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error { - argv := []string{"schedule:run"} - if v, ok := in.Flag("once"); ok { - if once, _ := strconv.ParseBool(v); once { - argv = append(argv, "--once") - } - } - argv = append(argv, in.Args()...) - return runAppBinary(ctx, argv, out) - }, - } -} - func runAppBinary(ctx context.Context, argv []string, out bonfire.Output) error { if len(argv) == 0 { return fmt.Errorf("summer: missing app command") diff --git a/cmd/summer/runtime_test.go b/cmd/summer/runtime_test.go index 7757df8..c3be3b4 100644 --- a/cmd/summer/runtime_test.go +++ b/cmd/summer/runtime_test.go @@ -3,6 +3,7 @@ package main import ( "context" "slices" + "strings" "testing" "git.golem15.com/golem15/summercms/modules/bonfire" @@ -42,3 +43,73 @@ func TestDelegatedServePassesFlags(t *testing.T) { } } } + +func TestDelegateCommandsPassArgs(t *testing.T) { + for _, args := range [][]string{ + {"migrate:rollback", "--plugin", "acme.blog"}, + {"queue:work", "--queue", "a", "--queue", "b"}, + {"schedule:run", "--once"}, + {"queue:clear", "emails"}, + {"migrate"}, + {"migrate:status", "--help"}, + {"route:list"}, + {"key:generate"}, + {"admin:create", "--email", "you@example.com", "--superuser"}, + {"admin:reset-password", "admin@example.com"}, + } { + got := captureAppRunner(t) + if out, err := runSummer(t, args...); err != nil { + t.Fatalf("%q: %v\n%s", args, err, out) + } + if !slices.Equal(*got, args) { + t.Fatalf("%q: argv = %q, want %q", args, *got, args) + } + } +} + +func TestDelegateCommandsHelp(t *testing.T) { + hints := map[string][]string{ + "migrate:rollback": {"--plugin"}, + "queue:work": {"--queue"}, + "schedule:run": {"--once"}, + "serve": {"--addr"}, + "admin:create": {"--email", "--superuser"}, + } + cmds := delegateCommands() + if len(cmds) != 11 { + t.Fatalf("delegateCommands() has %d commands, want 11", len(cmds)) + } + got := captureAppRunner(t) + for _, c := range cmds { + out, err := runSummer(t, "help", c.Name) + if err != nil { + t.Fatalf("help %s: %v", c.Name, err) + } + for _, want := range append([]string{"passed to the app binary"}, hints[c.Name]...) { + if !strings.Contains(out, want) { + t.Fatalf("help %s missing %q:\n%s", c.Name, want, out) + } + } + } + if *got != nil { + t.Fatalf("help ran the app binary with %q", *got) + } + + out, err := runSummer(t, "--help") + if err != nil { + t.Fatal(err) + } + for _, c := range cmds { + found := false + for _, line := range strings.Split(out, "\n") { + fields := strings.Fields(line) + if len(fields) > 0 && fields[0] == c.Name && strings.Contains(line, "(flags pass to the app binary)") { + found = true + break + } + } + if !found { + t.Fatalf("summer --help does not mark %s:\n%s", c.Name, out) + } + } +} diff --git a/docs/console/introduction.md b/docs/console/introduction.md index e67db20..cdbb9b6 100644 --- a/docs/console/introduction.md +++ b/docs/console/introduction.md @@ -26,6 +26,8 @@ summer make:model --help ./bin/acme migrate:rollback --help ``` +A command that `summer` delegates to the application binary, such as `summer serve`, answers `--help` with the binary's own help, so run it inside an application directory. Asking the help command of `summer` about a delegated command instead prints a short description and its common flags without running the binary. + ## Commands that summer delegates During development you often work from the application directory with `summer` alone. These `summer` commands find the application's `summer.yaml`, build `bin/` when it does not exist yet, and run the same command of the binary with the same arguments: @@ -39,8 +41,23 @@ During development you often work from the application directory with `summer` a | `summer queue:work` | `./bin/acme queue:work` | | `summer queue:clear` | `./bin/acme queue:clear` | | `summer schedule:run` | `./bin/acme schedule:run` | +| `summer route:list` | `./bin/acme route:list` | +| `summer key:generate` | `./bin/acme key:generate` | +| `summer admin:create` | `./bin/acme admin:create` | +| `summer admin:reset-password` | `./bin/acme admin:reset-password` | -`summer` does not rebuild an existing binary before it delegates. Run `summer build` after you change code, or keep `summer dev` running. Every other runtime command, such as `route:list`, `key:generate` or `admin:create`, is run on the binary directly. +`summer` parses no flags of these commands: every flag and argument, including `--help`, goes to the binary unchanged, and the binary's input is your terminal, so the password prompts of `admin:create` and `admin:reset-password` work as usual: + +```sh +summer serve --addr 127.0.0.1:8123 +summer serve --help +summer migrate:rollback --plugin acme.blog +summer admin:create --email admin@example.com --superuser +``` + +One leading `--` is ignored, so `summer serve -- --addr 127.0.0.1:8123` behaves the same as `summer serve --addr 127.0.0.1:8123`. + +`summer` does not rebuild an existing binary before it delegates. Run `summer build` after you change code, or keep `summer dev` running. Every other runtime command, such as `deferred:purge` or the commands your plugins add, is run on the binary directly. ## The sections of this chapter diff --git a/docs/console/setup-and-maintenance.md b/docs/console/setup-and-maintenance.md index 26a20aa..d3d272f 100644 --- a/docs/console/setup-and-maintenance.md +++ b/docs/console/setup-and-maintenance.md @@ -65,6 +65,8 @@ Copy the printed value into `SUMMER_APP__KEY`. When you rotate the key, keep the The default `--addr` listens on every interface. Pass a loopback address during development, and put a reverse proxy in front of the binary in production. The routing options are in [surf](../../modules/surf/README.md). +From the application directory, `summer serve --addr 127.0.0.1:8123` passes the flag through to the built binary. + ## Admin accounts | Command | Arguments and flags | Purpose | @@ -79,6 +81,8 @@ The default `--addr` listens on every interface. Pass a loopback address during Both commands prompt for the password without echoing it. In a script, pipe it on stdin, for example `printf '%s\n' "$ADMIN_PASSWORD" | ./bin/acme admin:create --email admin@example.com --superuser`. The `--password` flag still works but is deprecated and prints a warning, because the value stays in your shell history and is visible in the process list. The admin is described in [cabana](../../modules/cabana/README.md). +From the application directory, `summer admin:create --email admin@example.com --superuser` runs the same command on the built binary, prompt included. + ## Queues and the scheduler | Command | Arguments and flags | Purpose |