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:
@@ -10,6 +10,8 @@ import (
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/bonfire"
|
||||
)
|
||||
|
||||
// ArtifactResult is the outcome of a make:* command.
|
||||
@@ -46,7 +48,7 @@ func MakeModel(ctx context.Context, startDir, pluginID, name string, noMigration
|
||||
return ArtifactResult{}, err
|
||||
}
|
||||
if err := validateExportedIdent(name); err != nil {
|
||||
return ArtifactResult{}, fmt.Errorf("build: model: %w", err)
|
||||
return ArtifactResult{}, identHint(fmt.Errorf("build: model: %w", err))
|
||||
}
|
||||
table, err := modelTableName(plugin.ID, name)
|
||||
if err != nil {
|
||||
@@ -125,7 +127,7 @@ func MakeMigration(ctx context.Context, startDir, pluginID, name string) (Artifa
|
||||
return ArtifactResult{}, err
|
||||
}
|
||||
if err := validateExportedIdent(name); err != nil {
|
||||
return ArtifactResult{}, fmt.Errorf("build: migration: %w", err)
|
||||
return ArtifactResult{}, identHint(fmt.Errorf("build: migration: %w", err))
|
||||
}
|
||||
if err := rejectDuplicateIdent(plugin.Dir, "updates", name); err != nil {
|
||||
return ArtifactResult{}, err
|
||||
@@ -163,7 +165,7 @@ func MakeCommand(ctx context.Context, startDir, pluginID, name string) (Artifact
|
||||
return ArtifactResult{}, err
|
||||
}
|
||||
if err := validateExportedIdent(name); err != nil {
|
||||
return ArtifactResult{}, fmt.Errorf("build: command: %w", err)
|
||||
return ArtifactResult{}, identHint(fmt.Errorf("build: command: %w", err))
|
||||
}
|
||||
fn := name + "Command"
|
||||
if err := rejectDuplicateIdent(plugin.Dir, "console", fn); err != nil {
|
||||
@@ -201,7 +203,7 @@ func MakeJob(ctx context.Context, startDir, pluginID, name string) (ArtifactResu
|
||||
return ArtifactResult{}, err
|
||||
}
|
||||
if err := validateExportedIdent(name); err != nil {
|
||||
return ArtifactResult{}, fmt.Errorf("build: job: %w", err)
|
||||
return ArtifactResult{}, identHint(fmt.Errorf("build: job: %w", err))
|
||||
}
|
||||
fn := name + "Job"
|
||||
if err := rejectDuplicateIdent(plugin.Dir, "jobs", fn); err != nil {
|
||||
@@ -237,7 +239,7 @@ func MakeAdminController(ctx context.Context, startDir, pluginID, name string) (
|
||||
return ArtifactResult{}, err
|
||||
}
|
||||
if err := validateExportedIdent(name); err != nil {
|
||||
return ArtifactResult{}, fmt.Errorf("build: admin-controller: %w", err)
|
||||
return ArtifactResult{}, identHint(fmt.Errorf("build: admin-controller: %w", err))
|
||||
}
|
||||
fn := name + "Controller"
|
||||
if err := rejectDuplicateIdent(plugin.Dir, "controllers", fn); err != nil {
|
||||
@@ -325,6 +327,35 @@ func MakeAdminController(ctx context.Context, startDir, pluginID, name string) (
|
||||
return ArtifactResult{PluginDir: plugin.Dir, Files: created, Hint: plugin.Hint}, nil
|
||||
}
|
||||
|
||||
// pluginIDHint explains an invalid plugin ID.
|
||||
func pluginIDHint(err error) error {
|
||||
return bonfire.WithHint(err,
|
||||
"A plugin ID is the vendor and the plugin name in lower case letters and digits, joined by a dot.",
|
||||
"{bin} "+cliCommand("make:model")+" acme.blog <Name>")
|
||||
}
|
||||
|
||||
// identHint explains an invalid artifact name.
|
||||
func identHint(err error) error {
|
||||
return bonfire.WithHint(err,
|
||||
"The name becomes an exported Go identifier: a capital letter, then letters and digits.",
|
||||
"{bin} "+cliCommand("make:model")+" <vendor.plugin> BlogPost")
|
||||
}
|
||||
|
||||
// existsHint explains an artifact that is already there.
|
||||
func existsHint(err error) error {
|
||||
return bonfire.WithHint(err,
|
||||
"summer make: never overwrites a file or a name the plugin already has; choose another name, or edit the existing one.",
|
||||
"{bin} "+cliCommand("make:model")+" <vendor.plugin> <OtherName>")
|
||||
}
|
||||
|
||||
// pluginMissingHint explains a plugin that summer.yaml does not list and
|
||||
// plugins/ does not hold.
|
||||
func pluginMissingHint(err error, id string) error {
|
||||
return bonfire.WithHint(err,
|
||||
"The plugin is not listed in summer.yaml and has no directory under plugins/.",
|
||||
"{bin} make:plugin "+id+"\n{bin} plugin:add <dir>")
|
||||
}
|
||||
|
||||
func unexportedIdent(name string) string {
|
||||
r, size := utf8.DecodeRuneInString(name)
|
||||
return string(unicode.ToLower(r)) + name[size:]
|
||||
@@ -348,7 +379,7 @@ func resolvePlugin(startDir, id string) (pluginRef, error) {
|
||||
}
|
||||
}
|
||||
if err := ValidatePluginID(id); err != nil {
|
||||
return pluginRef{}, fmt.Errorf("build: %w", err)
|
||||
return pluginRef{}, pluginIDHint(fmt.Errorf("build: %w", err))
|
||||
}
|
||||
pluginDir, err := locatePluginDir(appDir, id)
|
||||
if err != nil {
|
||||
@@ -400,7 +431,9 @@ func inferPluginID(start, appDir string) (string, error) {
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
return "", fmt.Errorf("build: plugin id omitted and no plugin.go found from %s", start)
|
||||
return "", bonfire.WithHint(fmt.Errorf("build: plugin id omitted and no plugin.go found from %s", start),
|
||||
"Without a plugin ID, summer "+cliCommand("make:model")+" looks for plugin.go in this directory and above it.",
|
||||
"{bin} "+cliCommand("make:model")+" <vendor.plugin> <Name>")
|
||||
}
|
||||
|
||||
func locatePluginDir(appDir, id string) (string, error) {
|
||||
@@ -425,12 +458,12 @@ func locatePluginDir(appDir, id string) (string, error) {
|
||||
st, err := os.Stat(candidate)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("build: plugin %s not found", id)
|
||||
return "", pluginMissingHint(fmt.Errorf("build: plugin %s not found", id), id)
|
||||
}
|
||||
return "", fmt.Errorf("build: stat %s: %w", candidate, err)
|
||||
}
|
||||
if !st.IsDir() {
|
||||
return "", fmt.Errorf("build: plugin %s not found", id)
|
||||
return "", pluginMissingHint(fmt.Errorf("build: plugin %s not found", id), id)
|
||||
}
|
||||
return candidate, nil
|
||||
}
|
||||
@@ -492,7 +525,7 @@ func rejectDuplicateFile(pluginDir, rel string) error {
|
||||
return fmt.Errorf("build: artifact path %q escapes the plugin directory", rel)
|
||||
}
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return fmt.Errorf("build: %s already exists", rel)
|
||||
return existsHint(fmt.Errorf("build: %s already exists", rel))
|
||||
} else if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("build: stat %s: %w", path, err)
|
||||
}
|
||||
@@ -506,7 +539,7 @@ func rejectDuplicateIdent(pluginDir, pkg, ident string) error {
|
||||
}
|
||||
for _, ref := range refs {
|
||||
if ref.Ident == ident {
|
||||
return fmt.Errorf("build: %s.%s already exists", pkg, ident)
|
||||
return existsHint(fmt.Errorf("build: %s.%s already exists", pkg, ident))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/bonfire"
|
||||
"github.com/knadh/koanf/parsers/yaml"
|
||||
)
|
||||
|
||||
@@ -193,6 +194,34 @@ func ValidatePluginID(id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// cliInvocation is the summer command line being run (os.Args without the
|
||||
// program), shell-quoted, for the Try line of a hint.
|
||||
func cliInvocation() string {
|
||||
parts := make([]string, 0, len(os.Args))
|
||||
for _, a := range os.Args[1:] {
|
||||
if a == "" || strings.ContainsAny(a, " \t\n'\"\\$`*?[]{}()<>|&;#~!") {
|
||||
a = "'" + strings.ReplaceAll(a, "'", `'\''`) + "'"
|
||||
}
|
||||
parts = append(parts, a)
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
// cliCommand is the summer command being run (the first argument that is
|
||||
// not an option), or def when there is none, for hint texts.
|
||||
func cliCommand(def string) string {
|
||||
args := os.Args[1:]
|
||||
for i := 0; i < len(args); i++ {
|
||||
switch a := args[i]; {
|
||||
case a == "--env":
|
||||
i++
|
||||
case a != "" && !strings.HasPrefix(a, "-"):
|
||||
return a
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// FindAppDir walks up from start looking for summer.yaml.
|
||||
func FindAppDir(start string) (string, error) {
|
||||
return findAppDir(start)
|
||||
@@ -212,7 +241,9 @@ func findAppDir(start string) (string, error) {
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
return "", fmt.Errorf("build: no %s found from %s", manifestName, start)
|
||||
return "", bonfire.WithHint(fmt.Errorf("build: no %s found from %s", manifestName, start),
|
||||
"summer "+cliCommand("build")+" runs inside an application: a directory with summer.yaml, or one below it.",
|
||||
"cd <app>; {bin} "+cliInvocation())
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/bonfire"
|
||||
)
|
||||
|
||||
//go:embed stubs/*.tmpl
|
||||
@@ -63,7 +65,9 @@ func MakePlugin(ctx context.Context, startDir, id string) (string, error) {
|
||||
|
||||
func makePlugin(ctx context.Context, startDir, id string, opts pluginOptions) (string, error) {
|
||||
if err := ValidatePluginID(id); err != nil {
|
||||
return "", fmt.Errorf("build: %w", err)
|
||||
return "", bonfire.WithHint(fmt.Errorf("build: %w", err),
|
||||
"A plugin ID is the vendor and the plugin name in lower case letters and digits, joined by a dot.",
|
||||
"{bin} make:plugin acme.blog")
|
||||
}
|
||||
appDir, err := findAppDir(startDir)
|
||||
if err != nil {
|
||||
@@ -75,7 +79,9 @@ func makePlugin(ctx context.Context, startDir, id string, opts pluginOptions) (s
|
||||
return "", fmt.Errorf("build: plugin path %q escapes the app root", pluginDir)
|
||||
}
|
||||
if _, err := os.Stat(pluginDir); err == nil {
|
||||
return "", fmt.Errorf("build: plugin directory %s already exists", pluginDir)
|
||||
return "", bonfire.WithHint(fmt.Errorf("build: plugin directory %s already exists", pluginDir),
|
||||
"summer make:plugin creates a new directory under plugins/ and never overwrites one; register an existing plugin with plugin:add.",
|
||||
"{bin} plugin:add "+filepath.Join("plugins", name))
|
||||
} else if !os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("build: stat %s: %w", pluginDir, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user