feat(04-01): generate model, migration, and command artifacts

Add make:model, make:migration and make:command with plugin-id
inference, duplicate rejection, and a deterministic registry refresh
that leaves handwritten plugin.go untouched.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jakub Zych
2026-09-18 13:39:03 +02:00
parent 1edf9d7e4c
commit d8c56509f2
8 changed files with 1050 additions and 4 deletions

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
"strings"
"git.golem15.com/golem15/summercms/bonfire"
"git.golem15.com/golem15/summercms/internal/build"
@@ -27,6 +28,9 @@ func toolCommands() []bonfire.Command {
return []bonfire.Command{
buildCommand(),
makePluginCommand(),
makeModelCommand(),
makeMigrationCommand(),
makeCommandCommand(),
addPluginCommand(),
devCommand(),
parityProxyCommand(),
@@ -81,6 +85,122 @@ func makePluginCommand() bonfire.Command {
}
}
func makeModelCommand() bonfire.Command {
return bonfire.Command{
Name: "make:model",
Description: "Generate a GORM model and create-table migration",
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",
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",
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 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.Printf("created %s\n", path)
}
if result.Hint != "" {
out.Printf("%s\n", result.Hint)
}
return nil
}
func devCommand() bonfire.Command {
return bonfire.Command{
Name: "dev",

View File

@@ -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", "plugin:add", "dev", "migrate", "migrate:rollback", "migrate:status", "serve"} {
for _, want := range []string{"build", "make:plugin", "make:model", "make:migration", "make:command", "plugin:add", "dev", "migrate", "migrate:rollback", "migrate:status", "serve"} {
if !slices.Contains(names, want) {
t.Fatalf("missing %s in %v", want, names)
}