feat(summer): help, examples and hints for every tool command

- 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
This commit is contained in:
Jakub Zych
2026-10-21 13:25:53 +02:00
parent 9d06a78f62
commit 33460607db
14 changed files with 385 additions and 50 deletions

View File

@@ -31,10 +31,11 @@ func applicationCommands() []bonfire.Command {
}
// TestCommandsDocumented is the framework's documentation coverage gate
// (D-17): every framework command has a description, long help, examples
// and described arguments and options, and the test fails on any gap.
// (D-17): every command of the summer tool (its delegates included) and of
// an application binary has a description, long help, examples and
// described arguments and options, and the test fails on any gap.
func TestCommandsDocumented(t *testing.T) {
commands := applicationCommands()
commands := append(toolCommands(), applicationCommands()...)
for _, problem := range bonfire.CheckDocs(commands) {
t.Error(problem)
}

View File

@@ -2,7 +2,7 @@ package main
import (
"context"
"errors"
"fmt"
"os"
"os/signal"
"sync"
@@ -24,6 +24,17 @@ func docsBuildCommand() bonfire.Command {
return bonfire.Command{
Name: "docs:build",
Description: "Build the documentation site",
Long: `Checks every page under docs/ and every module README, then writes the static
site to site/ (or --out): HTML pages, a raw .md copy of each page, llms.txt,
llms-full.txt and the search index. It fails, writing nothing, when a page
names an identifier that does not exist, links to a missing page or anchor,
shows a command neither summer nor an application binary has, or has a src=
code block that differs from its source. --check only reports. Run it from
the framework root.`,
Examples: []bonfire.Example{
{Command: "docs:build --check", Description: "Check the docs and write nothing"},
{Command: "docs:build --base-url /docs/ --out /tmp/site", Description: "Build the site for /docs/ into /tmp/site"},
},
Flags: []bonfire.Flag{
{Name: "root", Description: "Repository root; src= paths and modules/ resolve against it", Default: "."},
{Name: "src", Description: "Docs source directory (default <root>/docs)"},
@@ -63,6 +74,14 @@ func docsSyncCommand() bonfire.Command {
return bonfire.Command{
Name: "docs:sync",
Description: "Rewrite src= code blocks from their sources",
Long: `Rewrites every code block that has a src= reference with the current text of
its source (a file, or a function, test or example in it), so the docs show
what the code does. Run it after changing code a page shows; docs:build
--check fails until you do.`,
Examples: []bonfire.Example{
{Command: "docs:sync", Description: "Refresh every src= block of the docs in this repository"},
{Command: "docs:sync --root ../summercms.go", Description: "Refresh the docs of another checkout"},
},
Flags: []bonfire.Flag{
{Name: "root", Description: "Repository root; src= paths resolve against it", Default: "."},
{Name: "src", Description: "Docs source directory (default <root>/docs)"},
@@ -174,11 +193,11 @@ func docsCommands() *docsite.Commands {
}
// reportDocsProblems prints one line per problem and a summary, and returns
// a short error so the binary exits 1.
// a Reported error, so the binary exits 1 without repeating the list.
func reportDocsProblems(out bonfire.Output, cmd string, problems []docsite.Problem) error {
for _, p := range problems {
out.Printf("%s\n", p)
}
out.Printf("%s: %d problems, nothing written\n", cmd, len(problems))
return errors.New(cmd + " failed")
return bonfire.Reported(fmt.Errorf("%s failed", cmd))
}

View File

@@ -1,6 +1,7 @@
package main
import (
"bytes"
"context"
"fmt"
"os"
@@ -54,12 +55,39 @@ 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
}
return build.App(ctx, dir, out)
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
},
}
}
@@ -68,6 +96,15 @@ 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",
@@ -86,7 +123,7 @@ func makePluginCommand() bonfire.Command {
if err != nil {
return err
}
out.Printf("created %s\n", created)
out.Success("Created " + created)
return nil
},
}
@@ -96,6 +133,17 @@ 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",
@@ -124,6 +172,15 @@ 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()
@@ -147,6 +204,16 @@ 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()
@@ -170,6 +237,14 @@ 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()
@@ -193,6 +268,16 @@ 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()
@@ -246,7 +331,7 @@ func flagTrue(in bonfire.Input, name string) bool {
func printArtifact(out bonfire.Output, result build.ArtifactResult) error {
for _, path := range result.Files {
out.Printf("created %s\n", path)
out.Success("Created " + path)
}
if result.Hint != "" {
out.Printf("%s\n", result.Hint)
@@ -287,6 +372,15 @@ 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",
@@ -304,7 +398,7 @@ func addPluginCommand() bonfire.Command {
if err := build.AddPlugin(ctx, dir, pluginDir); err != nil {
return err
}
out.Printf("added %s\n", pluginDir)
out.Success("Added " + pluginDir + " to summer.yaml, go.mod and go.work; run summer build")
return nil
},
}
@@ -314,6 +408,16 @@ 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",

View File

@@ -18,6 +18,16 @@ func parityProxyCommand() bonfire.Command {
return bonfire.Command{
Name: "parity:proxy",
Description: "Record named HTTP sessions through a loopback reverse proxy",
Long: `Runs a recording reverse proxy on a loopback address in front of the reference
backend (--upstream). Point a real client (the frontend, an MCP server) at it
and use it: each named session, taken from the X-Parity-Session request header
or --session, is written as one fixture under --fixtures (nuxt/ or mcp/).
Captured secrets go to the --vars file (mode 0600), never into fixtures.
Existing fixtures are kept unless --update is true. Ctrl+C stops it.`,
Examples: []bonfire.Example{
{Command: "parity:proxy --upstream http://127.0.0.1:8000 --fixtures testdata/parity --vars /tmp/parity/vars.yaml", Description: "Record sessions against a reference backend on port 8000"},
{Command: "parity:proxy --session checkout --rules testdata/parity/rules.yaml --vars /tmp/parity/vars.yaml", Description: "Record everything as the checkout session with capture rules"},
},
Flags: []bonfire.Flag{
{Name: "listen", Description: "Loopback bind address", Default: tide.DefaultListen},
{Name: "upstream", Description: "Fixed loopback HTTP origin", Default: tide.DefaultUpstream},
@@ -35,6 +45,16 @@ func parityRecordCommand() bonfire.Command {
return bonfire.Command{
Name: "parity:record",
Description: "Record HTTP responses for a one-flow YAML spec",
Long: `Sends the requests of a YAML spec (--spec) to the reference backend (--target)
and records the responses as a fixture (--output). With --manifest it instead
records the cases of a route manifest that have no fixture yet, at most
--next-batch (15) per run; --resume skips valid existing fixtures. Captured
secrets go to the --vars file. The true/false flags take the words true or
false.`,
Examples: []bonfire.Example{
{Command: "parity:record --spec testdata/parity/posts.spec.yaml --target http://127.0.0.1:8000 --output testdata/parity/posts.yaml --vars /tmp/parity/vars.yaml", Description: "Record one flow"},
{Command: "parity:record --manifest testdata/parity/routes.yaml --fixtures testdata/parity --target http://127.0.0.1:8000 --vars /tmp/parity/vars.yaml --next-batch 10", Description: "Record the next ten unrecorded routes of a manifest"},
},
Flags: []bonfire.Flag{
{Name: "spec", Description: "YAML request spec path"},
{Name: "target", Description: "Base URL of the HTTP backend"},
@@ -57,6 +77,15 @@ func parityReplayCommand() bonfire.Command {
return bonfire.Command{
Name: "parity:replay",
Description: "Replay recorded fixtures against an HTTP backend",
Long: `Replays recorded fixtures (a file or a directory, --fixtures) against a backend,
normally the port (--target), and reports each difference after masking IDs
and timestamps. --self-check fails on any mismatch; with --manifest and
--require-recorded a required case without a fixture fails too. It exits 1
when a fixture does not match.`,
Examples: []bonfire.Example{
{Command: "parity:replay --fixtures testdata/parity --target http://127.0.0.1:8080 --vars /tmp/parity/vars.yaml", Description: "Replay every fixture against the port"},
{Command: "parity:replay --fixtures testdata/parity/posts.yaml --target http://127.0.0.1:8000 --self-check true", Description: "Check a fixture against the reference backend itself"},
},
Flags: []bonfire.Flag{
{Name: "fixtures", Description: "Recorded YAML fixture path or directory"},
{Name: "target", Description: "Base URL of the HTTP backend"},
@@ -73,6 +102,17 @@ func parityBroadcastsCommand() bonfire.Command {
return bonfire.Command{
Name: "parity:broadcasts",
Description: "Record the Centrifugo publications a reference backend sends during a flow into a golden",
Long: `Starts a fake Centrifugo server on a loopback address (--listen), runs the flow
(--flow) against the reference backend (--target), which must be configured
to publish to it, and writes the publications it received into a golden file
(--out). --step records only one step, earlier steps running as setup; --ids
replaces id:* variables with placeholders; --settle waits for late
publications after each step; --pending marks the golden as recorded but not
yet asserted.`,
Examples: []bonfire.Example{
{Command: "parity:broadcasts --flow testdata/parity/chat.flow.yaml --target http://127.0.0.1:8000 --vars /tmp/parity/vars.yaml --out testdata/parity/chat.broadcasts.yaml", Description: "Record the publications of a chat flow"},
{Command: "parity:broadcasts --flow testdata/parity/chat.flow.yaml --target http://127.0.0.1:8000 --vars /tmp/parity/vars.yaml --out testdata/parity/send.broadcasts.yaml --step send --ids id:thread", Description: "Record one step with the thread id as a placeholder"},
},
Flags: []bonfire.Flag{
{Name: "flow", Description: "YAML flow spec to run against the target"},
{Name: "target", Description: "Loopback base URL of the reference backend"},
@@ -95,6 +135,16 @@ func parityUpstreamCommand() bonfire.Command {
return bonfire.Command{
Name: "parity:upstream",
Description: "Record the vendor HTTPS calls a reference backend sends through a loopback recording proxy into an upstream sidecar",
Long: `Runs a loopback HTTPS recording proxy (--listen) with a locally generated CA
kept in --ca-dir. Configure the reference backend to send its vendor API calls
(payments, mail, maps) through it and to trust the CA. In script mode each
call is answered from --script; in forward mode it is sent once to the real
vendor. When interrupted with Ctrl+C it writes the calls, credentials masked
with the --vars values, into the upstream sidecar --out.`,
Examples: []bonfire.Example{
{Command: "parity:upstream --ca-dir /tmp/parity/ca --out testdata/parity/pay.upstream.yaml --script testdata/parity/pay.script.yaml --vars /tmp/parity/vars.yaml", Description: "Answer vendor calls from a script"},
{Command: "parity:upstream --ca-dir /tmp/parity/ca --out testdata/parity/pay.upstream.yaml --mode forward --vars /tmp/parity/vars.yaml", Description: "Forward each call once to the real vendor"},
},
Flags: []bonfire.Flag{
{Name: "listen", Description: "Loopback address of the recording proxy", Default: tide.DefaultUpstreamProxyListen},
{Name: "ca-dir", Description: "Directory of the local parity CA, outside the fixtures tree (key kept mode 0600)"},

View File

@@ -21,29 +21,65 @@ import (
var appRunner = runAppBinary
// 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.
// table is the single source: name, base description, a hint naming the
// common flags or arguments the app binary accepts, and two examples.
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 <id>"},
{"migrate:status", "Show per-plugin migration history in the app binary", ""},
{"serve", "Run the app HTTP server", "--addr <host:port> (default :8080)"},
{"queue:work", "Run background job workers in the app binary", "--queue <name> (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 <email> (required), --login, --role <code>, --superuser; the password is prompted or read from stdin"},
{"admin:reset-password", "Reset a backend administrator's password in the app binary", "<identifier> (login or email); the password is prompted or read from stdin"},
{"admin:token:create", "Create a personal API token for a backend administrator in the app binary", "<identifier> --name <text> --scope <name> (repeatable) [--expires 30d|90d|365d|never|YYYY-MM-DD]"},
{"admin:token:list", "List personal API tokens in the app binary", "[<identifier>] [--revoked]"},
{"admin:token:revoke", "Revoke a personal API token by id in the app binary", "<id>"},
{"admin:token:scopes", "List available personal API token scopes in the app binary", ""},
ex := func(cmd, desc string) bonfire.Example { return bonfire.Example{Command: cmd, Description: desc} }
rows := []struct {
name, description, hint string
examples []bonfire.Example
}{
{"migrate", "Run plugin migrations in the app binary", "--pretend, --force", []bonfire.Example{
ex("migrate", "Apply every pending migration"),
ex("migrate --pretend", "List the pending migrations without applying them")}},
{"migrate:rollback", "Roll back the last migration of a plugin in the app binary", "--plugin <id>, --pretend, --force", []bonfire.Example{
ex("migrate:rollback --plugin acme.blog", "Roll back the last migration of acme.blog"),
ex("migrate:rollback --plugin acme.blog --pretend", "Name the migration that would roll back")}},
{"migrate:status", "Show per-plugin migration history in the app binary", "--json", []bonfire.Example{
ex("migrate:status", "Show the applied migrations per plugin"),
ex("migrate:status --json", "Machine-readable status")}},
{"serve", "Run the app HTTP server", "--addr <host:port> (default :8080)", []bonfire.Example{
ex("serve", "Serve on port 8080"),
ex("serve --addr 127.0.0.1:8123", "Serve on loopback port 8123")}},
{"queue:work", "Run background job workers in the app binary", "--queue <name> (repeatable)", []bonfire.Example{
ex("queue:work", "Work every known queue"),
ex("queue:work --queue mail", "Work only the mail queue")}},
{"queue:clear", "Clear pending queued jobs in the app binary", `[queue] argument (default "default"), --force`, []bonfire.Example{
ex("queue:clear", "Clear the default queue"),
ex("queue:clear imports --force", "Clear the imports queue without the production question")}},
{"schedule:run", "Run the scheduler in the app binary (or once for system cron)", "--once", []bonfire.Example{
ex("schedule:run", "Run the scheduler in the foreground"),
ex("schedule:run --once", "Run the entries due now and exit")}},
{"route:list", "List the app's routes from the app binary", "--json", []bonfire.Example{
ex("route:list", "Show every route"),
ex("route:list --json", "Machine-readable route list")}},
{"key:generate", "Print a fresh application key from the app binary", "--json", []bonfire.Example{
ex("key:generate", "Print a new key"),
ex("key:generate -q", "Print only the key")}},
{"admin:create", "Create a backend administrator in the app binary", "--email <email> (required), --login, --role <code>, --superuser; the password is prompted or read from stdin", []bonfire.Example{
ex("admin:create --email admin@example.com --superuser", "Create a superuser; the password is asked for"),
ex("admin:create --email editor@example.com --role editor", "Create an administrator with a role")}},
{"admin:reset-password", "Reset a backend administrator's password in the app binary", "<identifier> (login or email); the password is prompted or read from stdin", []bonfire.Example{
ex("admin:reset-password admin@example.com", "Reset a password; it is asked for"),
ex("admin:reset-password admin", "Reset by login")}},
{"admin:token:create", "Create a personal API token for a backend administrator in the app binary", "<identifier> --name <text> --scope <name> (repeatable) [--expires 30d|90d|365d|never|YYYY-MM-DD] [--json]", []bonfire.Example{
ex("admin:token:create jane --name agent --scope journal.posts", "A token limited to one scope"),
ex("admin:token:create jane --scope journal.posts --json", "The token and its details as JSON")}},
{"admin:token:list", "List personal API tokens in the app binary", "[<identifier>] [--revoked] [--json]", []bonfire.Example{
ex("admin:token:list", "Every active token"),
ex("admin:token:list jane --revoked", "Every token of one administrator")}},
{"admin:token:revoke", "Revoke a personal API token by id in the app binary", "<id>", []bonfire.Example{
ex("admin:token:revoke 12", "Revoke token 12"),
ex("admin:token:revoke 7 -q", "Revoke token 7 and print nothing but errors")}},
{"admin:token:scopes", "List available personal API token scopes in the app binary", "--json", []bonfire.Example{
ex("admin:token:scopes", "Show every scope"),
ex("admin:token:scopes --json", "Machine-readable scope list")}},
}
cmds := make([]bonfire.Command, 0, len(rows))
for _, r := range rows {
cmds = append(cmds, delegateCommand(r.name, r.description, r.hint))
c := delegateCommand(r.name, r.description, r.hint)
c.Examples = r.examples
cmds = append(cmds, c)
}
return cmds
}
@@ -53,7 +89,8 @@ func delegateCommands() []bonfire.Command {
func delegateCommand(name, description, hint string) bonfire.Command {
long := description + ".\n\nsummer runs bin/<binary> " + 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."
name + ", including --help, is passed to the app binary unchanged, and so are the global options (-v, -q, --ansi, --no-ansi, -n, --env). " +
"summer fails when the app binary fails."
if hint != "" {
long += "\n\nCommon flags: " + hint + "."
}