diff --git a/cmd/ninja/cmd/plugin.go b/cmd/ninja/cmd/plugin.go index 048da1a..f23ae18 100644 --- a/cmd/ninja/cmd/plugin.go +++ b/cmd/ninja/cmd/plugin.go @@ -208,14 +208,14 @@ func parsePrivateCoord(s string) (string, error) { return s, nil } rest := strings.TrimPrefix(s, "@") - slash := strings.IndexByte(rest, '/') - if slash < 0 { + before, after, ok := strings.Cut(rest, "/") + if !ok { return "", fmt.Errorf("expected @%s/, got %q", core.PrivateScopeSlug, s) } - if rest[:slash] != core.PrivateScopeSlug { - return "", fmt.Errorf("only @%s scope is supported for delete; got @%s", core.PrivateScopeSlug, rest[:slash]) + if before != core.PrivateScopeSlug { + return "", fmt.Errorf("only @%s scope is supported for delete; got @%s", core.PrivateScopeSlug, before) } - return rest[slash+1:], nil + return after, nil } func printPrivateSection(ctx context.Context, cli *orchclient.Client, hc creds.HostCreds) { @@ -605,26 +605,28 @@ func promptCategoriesWithDefault(ctx context.Context, cli *orchclient.Client, sc return out, nil } +// fetchPopularTagsForPrompt returns a comma-joined "tag (count)" string of the +// top tags for the given kind, or "" if the orchestrator lookup fails. The +// current implementation always returns "" because core's copy of the +// orchestrator proto bindings does not yet expose ListTags; once the bindings +// are regenerated this body becomes a best-effort PluginRegistryService.ListTags +// call. Callers must already tolerate an empty return. +func fetchPopularTagsForPrompt(ctx context.Context, cli *orchclient.Client, kind string) string { + _ = ctx + _ = cli + _ = kind + return "" +} + // promptTagsWithDefault prompts the user for free-form tags. Best-effort fetches // the top-20 most-used tags via ListTags(kind) to surface popular suggestions; // if that call fails (offline, etc.) it falls back to a plain prompt with a // one-line warning. Empty input keeps `current`. Validates with NormalizeTags; // on error, prints the issue and reprompts (one re-try, then bails). func promptTagsWithDefault(ctx context.Context, cli *orchclient.Client, scanner *bufio.Scanner, kind string, current []string) ([]string, error) { - // Best-effort popular-tags lookup. - var popular string - // TODO(tags): re-enable once core regenerates proto bindings to expose v1.ListTagsRequest (plan Task 12). - // if resp, err := cli.Reg.ListTags(ctx, connect.NewRequest(&v1.ListTagsRequest{Kind: kind, Limit: 20})); err == nil { - // parts := make([]string, 0, len(resp.Msg.Tags)) - // for _, t := range resp.Msg.Tags { - // parts = append(parts, fmt.Sprintf("%s (%d)", t.Tag, t.Count)) - // } - // popular = strings.Join(parts, ", ") - // } - _ = ctx - _ = cli + popular := fetchPopularTagsForPrompt(ctx, cli, kind) - for attempt := 0; attempt < 2; attempt++ { + for attempt := range 2 { if len(current) > 0 { fmt.Printf("Tags (current: %s)\n", strings.Join(current, ", ")) } else { @@ -1021,7 +1023,7 @@ func submodulePaths(repoDir string) []string { return nil } var paths []string - for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { + for line := range strings.SplitSeq(strings.TrimSpace(string(out)), "\n") { // each line is "submodule..path " fields := strings.Fields(line) if len(fields) >= 2 { @@ -1055,43 +1057,43 @@ func upsertPluginMod(scope, name, displayName, description, kind string, categor 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)) + fmt.Fprintf(&b, "name = %q\n", m.Plugin.Name) if m.Plugin.DisplayName != "" { - b.WriteString(fmt.Sprintf("display_name = %q\n", m.Plugin.DisplayName)) + fmt.Fprintf(&b, "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)) + fmt.Fprintf(&b, "scope = %q\n", m.Plugin.Scope) + fmt.Fprintf(&b, "version = %q\n", m.Plugin.Version) if m.Plugin.Description != "" { - b.WriteString(fmt.Sprintf("description = %q\n", m.Plugin.Description)) + fmt.Fprintf(&b, "description = %q\n", m.Plugin.Description) } if m.Plugin.Kind != "" { - b.WriteString(fmt.Sprintf("kind = %q\n", m.Plugin.Kind)) + fmt.Fprintf(&b, "kind = %q\n", m.Plugin.Kind) } if len(m.Plugin.Categories) > 0 { quoted := make([]string, len(m.Plugin.Categories)) for i, c := range m.Plugin.Categories { quoted[i] = fmt.Sprintf("%q", c) } - b.WriteString(fmt.Sprintf("categories = [%s]\n", strings.Join(quoted, ", "))) + fmt.Fprintf(&b, "categories = [%s]\n", strings.Join(quoted, ", ")) } if len(m.Plugin.Tags) > 0 { quoted := make([]string, len(m.Plugin.Tags)) for i, t := range m.Plugin.Tags { quoted[i] = fmt.Sprintf("%q", t) } - b.WriteString(fmt.Sprintf("tags = [%s]\n", strings.Join(quoted, ", "))) + fmt.Fprintf(&b, "tags = [%s]\n", strings.Join(quoted, ", ")) } if m.Plugin.Private { b.WriteString("private = true\n") } if m.Compatibility != nil { b.WriteString("\n[compatibility]\n") - b.WriteString(fmt.Sprintf("block_core = %q\n", m.Compatibility.BlockCore)) + fmt.Fprintf(&b, "block_core = %q\n", m.Compatibility.BlockCore) } for _, r := range m.Requires { b.WriteString("\n[[requires]]\n") - b.WriteString(fmt.Sprintf("name = %q\n", r.Name)) - b.WriteString(fmt.Sprintf("version = %q\n", r.Version)) + fmt.Fprintf(&b, "name = %q\n", r.Name) + fmt.Fprintf(&b, "version = %q\n", r.Version) } return os.WriteFile(path, []byte(b.String()), 0o644) } @@ -1108,7 +1110,7 @@ func gitignoredTrackedWarning(repoDir string, w io.Writer) { return } fmt.Fprintln(w, "warning: these tracked files match .gitignore and will still be shipped:") - for _, n := range strings.Split(names, "\n") { + for n := range strings.SplitSeq(names, "\n") { fmt.Fprintln(w, " "+n) } fmt.Fprintln(w, " (run `git rm --cached ` to drop)") @@ -1127,7 +1129,7 @@ func untrackedFilesWarning(repoDir string, w io.Writer) { return } fmt.Fprintln(w, "warning: these untracked files will NOT be in the archive:") - for _, n := range strings.Split(names, "\n") { + for n := range strings.SplitSeq(names, "\n") { fmt.Fprintln(w, " "+n) } fmt.Fprintln(w, " (run `git add ` if they should be shipped)") diff --git a/cmd/ninja/cmd/plugin_tags.go b/cmd/ninja/cmd/plugin_tags.go index 4690ac1..eaef161 100644 --- a/cmd/ninja/cmd/plugin_tags.go +++ b/cmd/ninja/cmd/plugin_tags.go @@ -39,24 +39,8 @@ func newPluginTagsCmd() *cobra.Command { return nil } cli := orchclient.New(resolvedHost, hc.Token) - _ = cli - - // TODO(tags): re-enable once core regenerates proto bindings to expose v1.ListTagsRequest (plan Task 12). - // resp, err := cli.Reg.ListTags(context.Background(), connect.NewRequest(&v1.ListTagsRequest{Kind: mod.Plugin.Kind, Limit: 20})) - // if err != nil { - // fmt.Println("(could not fetch popular tags — orchestrator unreachable)") - // return nil - // } - // if len(resp.Msg.Tags) == 0 { - // fmt.Println("Popular tags: (none yet)") - // return nil - // } - // parts := make([]string, len(resp.Msg.Tags)) - // for i, t := range resp.Msg.Tags { - // parts[i] = fmt.Sprintf("%s (%d)", t.Tag, t.Count) - // } - // fmt.Printf("Popular: %s\n", strings.Join(parts, ", ")) - fmt.Println("(popular tags unavailable — orchestrator bindings not yet regenerated)") + line := fetchPopularTagsForList(cli, mod.Plugin.Kind) + fmt.Println(line) return nil }, } @@ -167,3 +151,15 @@ func writeLocalModTags(mod *core.ModFile, tags []string) error { ) } +// fetchPopularTagsForList returns a single user-facing line listing the most-used +// tags for the given kind. The current body returns an unavailable-notice +// because core's copy of the orchestrator proto bindings does not yet expose +// ListTags; once bindings are regenerated this body becomes a real +// PluginRegistryService.ListTags call rendering "Popular: tag (count), ..." or +// "Popular tags: (none yet)". +func fetchPopularTagsForList(cli *orchclient.Client, kind string) string { + _ = cli + _ = kind + return "(popular tags unavailable — orchestrator bindings not yet regenerated)" +} +