fix(cli): init prompts default to existing plugin.mod values
This commit is contained in:
parent
041a7c2e3f
commit
06cabd6eb9
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user