fix(cli): init prompts default to existing plugin.mod values

This commit is contained in:
Alex Dunmow 2026-06-03 12:01:56 +08:00
parent 041a7c2e3f
commit 06cabd6eb9

View File

@ -54,24 +54,48 @@ func newPluginInitCmd() *cobra.Command {
ctx := context.Background()
scanner := bufio.NewScanner(os.Stdin)
if scope != "" {
// Read existing plugin.mod (if any) so prompts can default to its
// values. Missing or unparseable file is fine — we just start from
// empty defaults.
existing := &core.ModFile{}
if data, err := os.ReadFile("plugin.mod"); err == nil {
if m, err := core.ParseModFull(data); err == nil && m != nil {
existing = m
}
}
// Scope precedence: --scope flag > plugin.mod scope > interactive prompt.
switch {
case scope != "":
scope, err = parseScope(scope)
if err != nil {
return err
}
} else {
case existing.Plugin.Scope != "":
scope = "@" + strings.TrimPrefix(existing.Plugin.Scope, "@")
default:
scope, err = promptScope(ctx, cli, cr, resolvedHost, hc, scanner)
if err != nil {
return err
}
}
// Name precedence: --name flag > plugin.mod name > prompt.
if name == "" {
fmt.Print("Plugin name: ")
if existing.Plugin.Name != "" {
fmt.Printf("Plugin name [%s]: ", existing.Plugin.Name)
} else {
fmt.Print("Plugin name: ")
}
if !scanner.Scan() {
return fmt.Errorf("cancelled")
}
name = strings.TrimSpace(scanner.Text())
input := strings.TrimSpace(scanner.Text())
if input != "" {
name = input
} else {
name = existing.Plugin.Name
}
if name == "" {
return fmt.Errorf("plugin name is required")
}
@ -79,28 +103,37 @@ func newPluginInitCmd() *cobra.Command {
rawName := name
name = strings.ToLower(name)
fmt.Printf("Display name [%s]: ", rawName)
displayName := ""
if scanner.Scan() {
displayName = strings.TrimSpace(scanner.Text())
displayDefault := existing.Plugin.DisplayName
if displayDefault == "" {
displayDefault = rawName
}
if displayName == "" {
displayName = rawName
fmt.Printf("Display name [%s]: ", displayDefault)
displayName := displayDefault
if scanner.Scan() {
if v := strings.TrimSpace(scanner.Text()); v != "" {
displayName = v
}
}
fmt.Print("Description (one-line summary, optional): ")
description := ""
descLabel := "Description (one-line summary, optional)"
if existing.Plugin.Description != "" {
descLabel = fmt.Sprintf("Description [%s]", truncate(existing.Plugin.Description, 60))
}
fmt.Printf("%s: ", descLabel)
description := existing.Plugin.Description
if scanner.Scan() {
description = strings.TrimSpace(scanner.Text())
if v := strings.TrimSpace(scanner.Text()); v != "" {
description = v
}
}
kind, err := promptKind(scanner)
kind, err := promptKindWithDefault(scanner, existing.Plugin.Kind)
if err != nil {
return err
}
var cats []string
if kind == "plugin" {
cats, err = promptCategories(ctx, cli, scanner)
cats, err = promptCategoriesWithDefault(ctx, cli, scanner, existing.Plugin.Categories)
if err != nil {
return err
}
@ -233,15 +266,27 @@ func createScopeInline(ctx context.Context, cli *orchclient.Client, scanner *buf
return slug, nil
}
func promptKind(scanner *bufio.Scanner) (string, error) {
// promptKindWithDefault prompts for plugin kind. When current is "plugin" or
// "theme", that becomes the empty-input default (and the [n] hint reflects it).
func promptKindWithDefault(scanner *bufio.Scanner, current string) (string, error) {
defaultIdx := "1"
switch current {
case "theme":
defaultIdx = "2"
case "plugin", "":
defaultIdx = "1"
}
fmt.Println("Kind: 1) plugin 2) theme")
fmt.Print("Select [1]: ")
fmt.Printf("Select [%s]: ", defaultIdx)
if !scanner.Scan() {
return "", fmt.Errorf("cancelled")
}
v := strings.TrimSpace(scanner.Text())
if v == "" {
v = defaultIdx
}
switch v {
case "", "1", "plugin":
case "1", "plugin":
return "plugin", nil
case "2", "theme":
return "theme", nil
@ -250,7 +295,20 @@ func promptKind(scanner *bufio.Scanner) (string, error) {
}
}
func promptCategories(ctx context.Context, cli *orchclient.Client, scanner *bufio.Scanner) ([]string, error) {
// truncate clips a string to at most max runes, appending "…" if it was cut.
// Used to keep prompt labels readable when the existing description is long.
func truncate(s string, max int) string {
if len([]rune(s)) <= max {
return s
}
r := []rune(s)
return string(r[:max-1]) + "…"
}
// promptCategoriesWithDefault lists the canonical categories and lets the user
// pick by number. When current is non-empty, those slugs are pre-selected:
// the prompt shows their numbers as the default, and empty input keeps them.
func promptCategoriesWithDefault(ctx context.Context, cli *orchclient.Client, scanner *bufio.Scanner, current []string) ([]string, error) {
resp, err := cli.Reg.ListCategories(ctx, connect.NewRequest(&v1.ListCategoriesRequest{}))
if err != nil {
return nil, fmt.Errorf("list categories: %w", err)
@ -259,17 +317,41 @@ func promptCategories(ctx context.Context, cli *orchclient.Client, scanner *bufi
if len(cats) == 0 {
return nil, nil
}
fmt.Println("Categories (comma-separated numbers, or blank to skip):")
// Build the default indices string from `current` so the prompt shows e.g.
// "Select [9]: " when current is ["templates"] and templates is the 9th
// entry.
slugToIdx := make(map[string]int, len(cats))
for i, c := range cats {
fmt.Printf(" %d. %s — %s\n", i+1, c.Slug, c.DisplayName)
slugToIdx[c.Slug] = i + 1
}
var defaultNums []string
for _, slug := range current {
if i, ok := slugToIdx[slug]; ok {
defaultNums = append(defaultNums, strconv.Itoa(i))
}
}
defaultLabel := strings.Join(defaultNums, ",")
fmt.Println("Categories (comma-separated numbers, or blank to keep current):")
for i, c := range cats {
marker := " "
if _, picked := pickedSet(current)[c.Slug]; picked {
marker = "*"
}
fmt.Printf(" %d.%s%s — %s\n", i+1, marker, c.Slug, c.DisplayName)
}
if defaultLabel != "" {
fmt.Printf("Select [%s]: ", defaultLabel)
} else {
fmt.Print("Select: ")
}
fmt.Print("Select: ")
if !scanner.Scan() {
return nil, fmt.Errorf("cancelled")
}
raw := strings.TrimSpace(scanner.Text())
if raw == "" {
return nil, nil
// Empty input → keep current (or none, if there's no current).
return current, nil
}
parts := strings.Split(raw, ",")
out := make([]string, 0, len(parts))
@ -283,6 +365,15 @@ func promptCategories(ctx context.Context, cli *orchclient.Client, scanner *bufi
return out, nil
}
// pickedSet returns a set of the given slugs for O(1) membership checks.
func pickedSet(slugs []string) map[string]struct{} {
s := make(map[string]struct{}, len(slugs))
for _, x := range slugs {
s[x] = struct{}{}
}
return s
}
func newPluginPublishCmd() *cobra.Command {
var channel string
var allowDirty bool