diff --git a/cmd/ninja/cmd/plugin.go b/cmd/ninja/cmd/plugin.go index 305fb43..a82a482 100644 --- a/cmd/ninja/cmd/plugin.go +++ b/cmd/ninja/cmd/plugin.go @@ -76,6 +76,23 @@ func newPluginInitCmd() *cobra.Command { return fmt.Errorf("plugin name is required") } } + rawName := name + name = strings.ToLower(name) + + fmt.Printf("Display name [%s]: ", rawName) + displayName := "" + if scanner.Scan() { + displayName = strings.TrimSpace(scanner.Text()) + } + if displayName == "" { + displayName = rawName + } + + fmt.Print("Description (one-line summary, optional): ") + description := "" + if scanner.Scan() { + description = strings.TrimSpace(scanner.Text()) + } kind, err := promptKind(scanner) if err != nil { @@ -92,7 +109,8 @@ func newPluginInitCmd() *cobra.Command { if _, err := cli.Reg.CreatePlugin(ctx, connect.NewRequest(&v1.CreatePluginRequest{ ScopeSlug: scopeAPISlug(scope), Name: name, - Description: "", + DisplayName: displayName, + Description: description, Kind: kind, Categories: cats, })); err != nil { @@ -100,7 +118,7 @@ func newPluginInitCmd() *cobra.Command { } fmt.Printf("\nCreated %s/%s\n", scope, name) - if err := upsertPluginMod(scope, name, kind, cats); err != nil { + if err := upsertPluginMod(scope, name, displayName, description, kind, cats); err != nil { return err } fmt.Println("plugin.mod updated") @@ -576,7 +594,7 @@ func submodulePaths(repoDir string) []string { return paths } -func upsertPluginMod(scope, name, kind string, categories []string) error { +func upsertPluginMod(scope, name, displayName, description, kind string, categories []string) error { const file = "plugin.mod" existing, _ := os.ReadFile(file) mod, _ := core.ParseModFull(existing) @@ -588,6 +606,8 @@ func upsertPluginMod(scope, name, kind string, categories []string) error { } mod.Plugin.Scope = scope mod.Plugin.Name = name + mod.Plugin.DisplayName = displayName + mod.Plugin.Description = description mod.Plugin.Kind = kind mod.Plugin.Categories = categories return writeMod(file, mod) @@ -597,8 +617,14 @@ func writeMod(path string, m *core.ModFile) error { var b strings.Builder b.WriteString("[plugin]\n") b.WriteString(fmt.Sprintf("name = %q\n", m.Plugin.Name)) + if m.Plugin.DisplayName != "" { + b.WriteString(fmt.Sprintf("display_name = %q\n", m.Plugin.DisplayName)) + } b.WriteString(fmt.Sprintf("scope = %q\n", m.Plugin.Scope)) b.WriteString(fmt.Sprintf("version = %q\n", m.Plugin.Version)) + if m.Plugin.Description != "" { + b.WriteString(fmt.Sprintf("description = %q\n", m.Plugin.Description)) + } if m.Plugin.Kind != "" { b.WriteString(fmt.Sprintf("kind = %q\n", m.Plugin.Kind)) } diff --git a/internal/api/orchestrator/v1/plugin_registry.pb.go b/internal/api/orchestrator/v1/plugin_registry.pb.go index 671d940..3470ec1 100644 --- a/internal/api/orchestrator/v1/plugin_registry.pb.go +++ b/internal/api/orchestrator/v1/plugin_registry.pb.go @@ -102,6 +102,7 @@ type Plugin struct { Categories []string `protobuf:"bytes,8,rep,name=categories,proto3" json:"categories,omitempty"` UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` Kind string `protobuf:"bytes,10,opt,name=kind,proto3" json:"kind,omitempty"` + DisplayName string `protobuf:"bytes,11,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -206,6 +207,13 @@ func (x *Plugin) GetKind() string { return "" } +func (x *Plugin) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + type Category struct { state protoimpl.MessageState `protogen:"open.v1"` Slug string `protobuf:"bytes,1,opt,name=slug,proto3" json:"slug,omitempty"` @@ -713,6 +721,7 @@ type CreatePluginRequest struct { Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` Kind string `protobuf:"bytes,4,opt,name=kind,proto3" json:"kind,omitempty"` Categories []string `protobuf:"bytes,5,rep,name=categories,proto3" json:"categories,omitempty"` + DisplayName string `protobuf:"bytes,6,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -782,6 +791,13 @@ func (x *CreatePluginRequest) GetCategories() []string { return nil } +func (x *CreatePluginRequest) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + type CreatePluginResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Plugin *Plugin `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` @@ -2188,7 +2204,7 @@ const file_orchestrator_v1_plugin_registry_proto_rawDesc = "" + "\x04slug\x18\x02 \x01(\tR\x04slug\x12!\n" + "\fdisplay_name\x18\x03 \x01(\tR\vdisplayName\x129\n" + "\n" + - "created_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\"\xb9\x02\n" + + "created_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\"\xdc\x02\n" + "\x06Plugin\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x1d\n" + "\n" + @@ -2206,7 +2222,8 @@ const file_orchestrator_v1_plugin_registry_proto_rawDesc = "" + "\n" + "updated_at\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\x12\x12\n" + "\x04kind\x18\n" + - " \x01(\tR\x04kind\"\x82\x01\n" + + " \x01(\tR\x04kind\x12!\n" + + "\fdisplay_name\x18\v \x01(\tR\vdisplayName\"\x82\x01\n" + "\bCategory\x12\x12\n" + "\x04slug\x18\x01 \x01(\tR\x04slug\x12!\n" + "\fdisplay_name\x18\x02 \x01(\tR\vdisplayName\x12 \n" + @@ -2240,7 +2257,7 @@ const file_orchestrator_v1_plugin_registry_proto_rawDesc = "" + "\x04slug\x18\x01 \x01(\tR\x04slug\"s\n" + "\x10GetScopeResponse\x12,\n" + "\x05scope\x18\x01 \x01(\v2\x16.orchestrator.v1.ScopeR\x05scope\x121\n" + - "\aplugins\x18\x02 \x03(\v2\x17.orchestrator.v1.PluginR\aplugins\"\x9e\x01\n" + + "\aplugins\x18\x02 \x03(\v2\x17.orchestrator.v1.PluginR\aplugins\"\xc1\x01\n" + "\x13CreatePluginRequest\x12\x1d\n" + "\n" + "scope_slug\x18\x01 \x01(\tR\tscopeSlug\x12\x12\n" + @@ -2249,7 +2266,8 @@ const file_orchestrator_v1_plugin_registry_proto_rawDesc = "" + "\x04kind\x18\x04 \x01(\tR\x04kind\x12\x1e\n" + "\n" + "categories\x18\x05 \x03(\tR\n" + - "categories\"G\n" + + "categories\x12!\n" + + "\fdisplay_name\x18\x06 \x01(\tR\vdisplayName\"G\n" + "\x14CreatePluginResponse\x12/\n" + "\x06plugin\x18\x01 \x01(\v2\x17.orchestrator.v1.PluginR\x06plugin\"E\n" + "\x10GetPluginRequest\x12\x1d\n" + diff --git a/plugin/mod.go b/plugin/mod.go index 29fc4dc..3e22f4c 100644 --- a/plugin/mod.go +++ b/plugin/mod.go @@ -13,7 +13,15 @@ type ModFile struct { } type ModPlugin struct { + // Name is the lowercase identifier used for the plugin slug in URLs and DB + // lookups. The CLI normalises this on write; the registry normalises on + // create. Use DisplayName for human-readable presentation. Name string `toml:"name"` + // DisplayName is the human-readable form (any case). Optional; if empty + // the registry falls back to the input name with its original case. + DisplayName string `toml:"display_name,omitempty"` + // Description is the short summary surfaced in the registry. Optional. + Description string `toml:"description,omitempty"` // Scope is the plugin owner namespace as it appears in plugin.mod. It may // include the leading "@" (e.g. "@themes") or omit it (e.g. "themes") — // both forms are accepted. Consumers comparing scopes should trim the "@" diff --git a/proto/orchestrator/v1/plugin_registry.proto b/proto/orchestrator/v1/plugin_registry.proto index 4859514..cc6e9e7 100644 --- a/proto/orchestrator/v1/plugin_registry.proto +++ b/proto/orchestrator/v1/plugin_registry.proto @@ -58,6 +58,7 @@ message Plugin { repeated string categories = 8; google.protobuf.Timestamp updated_at = 9; string kind = 10; + string display_name = 11; } message Category { @@ -101,6 +102,7 @@ message CreatePluginRequest { string description = 3; string kind = 4; repeated string categories = 5; + string display_name = 6; } message CreatePluginResponse { Plugin plugin = 1;