Skip to content

Commit 60666d3

Browse files
committed
Restructure CLI: consolidate commands and add colorized help
- Move search/show under catalog subcommand (apx catalog search, apx catalog show) - Merge link into sync — apx sync now handles Go and Python overlays - Flatten semver suggest to apx semver - Add colorized help output with ANSI-aware formatting - Add Unlinker interface and ClearWorkFile for sync --clean - Hide fetch command, improve CI/color detection in internal/ui - Update docs, tests, and testscripts to match
1 parent c97f84c commit 60666d3

42 files changed

Lines changed: 565 additions & 317 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Makefile

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,24 @@ GORELEASER_VERSION := v2.6.1
2626

2727
## help: Show this help message
2828
help:
29-
@echo "Usage: make [target]"
30-
@echo ""
31-
@echo "Available targets:"
32-
@awk '/^##/ { print " " $$0 }' $(MAKEFILE_LIST) | sed 's/##//' | sort
29+
@if [ -z "$$CI$$GITHUB_ACTIONS$$JENKINS_HOME$$NO_COLOR" ]; then \
30+
BOLD="\033[1m"; CYAN="\033[36m"; RESET="\033[0m"; \
31+
else \
32+
BOLD=""; CYAN=""; RESET=""; \
33+
fi; \
34+
printf "$${BOLD}Usage:$${RESET} make [target]\n"; \
35+
printf "\n"; \
36+
printf "$${BOLD}Available targets:$${RESET}\n"; \
37+
awk -v cyan="$${CYAN}" -v reset="$${RESET}" \
38+
'/^## / { \
39+
line = substr($$0, 4); \
40+
colon = index(line, ":"); \
41+
if (colon > 0) { \
42+
target = substr(line, 1, colon-1); \
43+
desc = substr(line, colon+2); \
44+
printf " %s%-22s%s %s\n", cyan, target, reset, desc; \
45+
} \
46+
}' $(MAKEFILE_LIST) | sort
3347

3448
## build: Build the binary
3549
build:

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,7 @@ jobs:
454454
fetch-depth: 0
455455
456456
- name: Install APX
457-
uses: infobloxopen/apx@v1
457+
uses: infobloxopen/apx@main
458458
459459
- name: Lint Schemas
460460
run: apx lint internal/apis
@@ -473,7 +473,7 @@ jobs:
473473
token: ${{ secrets.CANONICAL_REPO_TOKEN }}
474474
475475
- name: Install APX
476-
uses: infobloxopen/apx@v1
476+
uses: infobloxopen/apx@main
477477
478478
- name: Release to Canonical Repo
479479
run: |

cmd/apx/commands/breaking.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ func newBreakingCmd() *cobra.Command {
1818
Args: cobra.MaximumNArgs(1),
1919
RunE: breakingAction,
2020
}
21-
cmd.Flags().String("against", "", "git reference or path to compare against")
22-
_ = cmd.MarkFlagRequired("against")
21+
cmd.Flags().String("against", "", "git reference or path to compare against (required)")
2322
cmd.Flags().StringP("format", "f", "", "Schema format (proto, openapi, avro, jsonschema, parquet)")
2423
return cmd
2524
}
@@ -31,6 +30,9 @@ func breakingAction(cmd *cobra.Command, args []string) error {
3130
}
3231

3332
against, _ := cmd.Flags().GetString("against")
33+
if against == "" {
34+
return fmt.Errorf("--against is required\n\nUsage: apx breaking [path] --against <git-ref>\n\nExamples:\n apx breaking --against HEAD^\n apx breaking --against origin/main")
35+
}
3436

3537
// Try to resolve API ID (e.g. proto/payments/ledger/v1) to a path.
3638
// Falls back to treating the argument as a filesystem path.

cmd/apx/commands/catalog.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ import (
1414
func newCatalogCmd() *cobra.Command {
1515
cmd := &cobra.Command{
1616
Use: "catalog",
17-
Short: "Catalog operations",
17+
Short: "Browse and search the API catalog",
1818
}
19+
cmd.AddCommand(newCatalogSearchCmd())
20+
cmd.AddCommand(newCatalogShowCmd())
1921
cmd.AddCommand(newCatalogGenerateCmd())
2022
cmd.AddCommand(newCatalogSiteCmd())
2123
return cmd

cmd/apx/commands/doc_parity_test.go

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -160,23 +160,23 @@ func TestDocParity_ConsumerCommands(t *testing.T) {
160160
root := NewRootCmd("test")
161161

162162
tests := []struct {
163-
name string
164-
commandName string
165-
section string
163+
name string
164+
args []string
165+
section string
166166
}{
167-
{"search command", "search", "6 - Discover APIs"},
168-
{"add command", "add", "6 - Add Dependencies"},
169-
{"gen command", "gen", "6 - Generate Client Code"},
170-
{"sync command", "sync", "6 - Generate Client Code"},
171-
{"unlink command", "unlink", "6 - Switch to Published Module"},
167+
{"search command", []string{"catalog", "search"}, "6 - Discover APIs"},
168+
{"add command", []string{"add"}, "6 - Add Dependencies"},
169+
{"gen command", []string{"gen"}, "6 - Generate Client Code"},
170+
{"sync command", []string{"sync"}, "6 - Generate Client Code"},
171+
{"unlink command", []string{"unlink"}, "6 - Switch to Published Module"},
172172
}
173173

174174
for _, tt := range tests {
175175
t.Run(tt.name, func(t *testing.T) {
176-
cmd, _, err := root.Find([]string{tt.commandName})
176+
cmd, _, err := root.Find(tt.args)
177177
if err != nil || cmd.Use == "apx" {
178178
t.Fatalf("Doc parity failure: 'apx %s' command not found (documented in quickstart.md section %s)",
179-
tt.commandName, tt.section)
179+
strings.Join(tt.args, " "), tt.section)
180180
}
181181
})
182182
}
@@ -233,14 +233,14 @@ func TestDocParity_AllCommandsExist(t *testing.T) {
233233
commands := [][]string{
234234
{"init"}, {"init", "canonical"}, {"init", "app"},
235235
{"lint"}, {"breaking"},
236-
{"semver"}, {"semver", "suggest"},
236+
{"semver"},
237237
{"gen"}, {"policy"}, {"policy", "check"},
238238
{"catalog"},
239-
{"release"}, {"search"}, {"add"},
239+
{"release"}, {"add"},
240240
{"sync"}, {"unlink"}, {"update"}, {"upgrade"},
241241
{"config"}, {"config", "init"}, {"config", "validate"},
242242
{"fetch"},
243-
{"show"},
243+
{"catalog", "search"}, {"catalog", "show"},
244244
{"inspect"}, {"inspect", "identity"}, {"inspect", "release"},
245245
{"explain"}, {"explain", "go-path"},
246246
// Note: "completion" is Cobra's built-in command, added lazily at Execute() time;
@@ -268,9 +268,9 @@ func TestDocParity_AllFlagsExist(t *testing.T) {
268268
root := NewRootCmd("test")
269269
documentedFlags := []flagSpec{
270270
{[]string{"breaking"}, []string{"against", "format"}},
271-
{[]string{"semver", "suggest"}, []string{"against"}},
271+
{[]string{"semver"}, []string{"against"}},
272272
{[]string{"gen"}, []string{"out", "clean", "manifest"}},
273-
{[]string{"search"}, []string{"format", "catalog", "lifecycle", "domain", "api-line", "origin", "tag"}},
273+
{[]string{"catalog", "search"}, []string{"format", "catalog", "lifecycle", "domain", "api-line", "origin", "tag"}},
274274
{[]string{"sync"}, []string{"clean", "dry-run"}},
275275
{[]string{"fetch"}, []string{"config", "output", "verify"}},
276276
{[]string{"lint"}, []string{"format"}},
@@ -313,14 +313,14 @@ func TestDocParity_RequiredFlags(t *testing.T) {
313313
}
314314
})
315315

316-
t.Run("semver suggest requires --against", func(t *testing.T) {
316+
t.Run("semver requires --against", func(t *testing.T) {
317317
root := NewRootCmd("test")
318-
root.SetArgs([]string{"semver", "suggest", "."})
318+
root.SetArgs([]string{"semver", "."})
319319
var errBuf strings.Builder
320320
root.SetErr(&errBuf)
321321
err := root.Execute()
322322
if err == nil {
323-
t.Error("Doc parity failure: 'apx semver suggest .' should fail when --against is not provided")
323+
t.Error("Doc parity failure: 'apx semver .' should fail when --against is not provided")
324324
}
325325
})
326326
}
@@ -441,7 +441,7 @@ func TestDocParity_CommandExamples(t *testing.T) {
441441
{"breaking", []string{"apx", "breaking"}},
442442
{"gen go", []string{"apx", "gen", "go"}},
443443
{"sync", []string{"apx", "sync"}},
444-
{"search", []string{"apx", "search", "payments"}},
444+
{"catalog search", []string{"apx", "catalog", "search", "payments"}},
445445
{"add", []string{"apx", "add", "proto/payments/ledger/v1@v1.2.3"}},
446446
{"unlink", []string{"apx", "unlink", "proto/payments/ledger/v1"}},
447447
}

cmd/apx/commands/fetch.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import (
1111

1212
func newFetchCmd() *cobra.Command {
1313
cmd := &cobra.Command{
14-
Use: "fetch",
15-
Short: "Download and cache toolchain dependencies for offline use",
16-
RunE: fetchAction,
14+
Use: "fetch",
15+
Short: "Download and cache toolchain dependencies for offline use",
16+
Hidden: true,
17+
RunE: fetchAction,
1718
}
1819
cmd.Flags().StringP("config", "c", "apx.yaml", "Path to configuration file")
1920
cmd.Flags().String("output", ".apx-tools", "Output directory for toolchain bundles")

cmd/apx/commands/help.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package commands
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/fatih/color"
8+
"github.com/infobloxopen/apx/internal/ui"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
// setColorizedHelp installs a colorized help function on cmd.
13+
// Cobra walks the parent chain when looking up HelpFunc, so every subcommand
14+
// inherits this automatically without needing per-command wiring.
15+
func setColorizedHelp(cmd *cobra.Command) {
16+
cmd.SetHelpFunc(colorHelpFunc)
17+
}
18+
19+
func colorHelpFunc(cmd *cobra.Command, _ []string) {
20+
// Honor --no-color even when PersistentPreRunE hasn't run yet
21+
// (e.g. `apx --no-color --help` triggers help before the pre-run hook).
22+
if noColor, err := cmd.Root().PersistentFlags().GetBool("no-color"); err == nil && noColor {
23+
ui.SetColorEnabled(false)
24+
}
25+
26+
out := cmd.OutOrStdout()
27+
bold := color.New(color.Bold)
28+
cyan := color.New(color.FgCyan)
29+
30+
// Description — long preferred, fall back to short.
31+
desc := cmd.Long
32+
if desc == "" {
33+
desc = cmd.Short
34+
}
35+
if desc != "" {
36+
fmt.Fprintln(out, strings.TrimRight(desc, " \n"))
37+
fmt.Fprintln(out)
38+
}
39+
40+
// Usage line
41+
fmt.Fprintf(out, "%s\n", bold.Sprint("Usage:"))
42+
if cmd.Runnable() {
43+
fmt.Fprintf(out, " %s\n", cmd.UseLine())
44+
}
45+
if cmd.HasAvailableSubCommands() {
46+
fmt.Fprintf(out, " %s [command]\n", cmd.CommandPath())
47+
}
48+
49+
// Aliases
50+
if len(cmd.Aliases) > 0 {
51+
fmt.Fprintln(out)
52+
fmt.Fprintf(out, "%s\n", bold.Sprint("Aliases:"))
53+
fmt.Fprintf(out, " %s\n", cmd.NameAndAliases())
54+
}
55+
56+
// Examples
57+
if cmd.HasExample() {
58+
fmt.Fprintln(out)
59+
fmt.Fprintf(out, "%s\n", bold.Sprint("Examples:"))
60+
fmt.Fprintln(out, cmd.Example)
61+
}
62+
63+
// Available Commands
64+
if cmd.HasAvailableSubCommands() {
65+
fmt.Fprintln(out)
66+
fmt.Fprintf(out, "%s\n", bold.Sprint("Available Commands:"))
67+
pad := subcommandPadding(cmd)
68+
for _, sub := range cmd.Commands() {
69+
if sub.IsAvailableCommand() || sub.Name() == "help" {
70+
// Compute spacing manually — cyan.Sprint embeds ANSI codes that
71+
// would throw off %-*s width calculation.
72+
spacing := strings.Repeat(" ", max(0, pad-len(sub.Name())))
73+
fmt.Fprintf(out, " %s%s %s\n", cyan.Sprint(sub.Name()), spacing, sub.Short)
74+
}
75+
}
76+
}
77+
78+
// Local Flags
79+
if cmd.HasAvailableLocalFlags() {
80+
fmt.Fprintln(out)
81+
fmt.Fprintf(out, "%s\n", bold.Sprint("Flags:"))
82+
fmt.Fprint(out, strings.TrimRight(cmd.LocalFlags().FlagUsages(), "\n"))
83+
fmt.Fprintln(out)
84+
}
85+
86+
// Global / Inherited Flags
87+
if cmd.HasAvailableInheritedFlags() {
88+
fmt.Fprintln(out)
89+
fmt.Fprintf(out, "%s\n", bold.Sprint("Global Flags:"))
90+
fmt.Fprint(out, strings.TrimRight(cmd.InheritedFlags().FlagUsages(), "\n"))
91+
fmt.Fprintln(out)
92+
}
93+
94+
// Additional help topics (commands marked as help topics, not runnable)
95+
if cmd.HasHelpSubCommands() {
96+
fmt.Fprintln(out)
97+
fmt.Fprintf(out, "%s\n", bold.Sprint("Additional help topics:"))
98+
pad := subcommandPadding(cmd)
99+
for _, sub := range cmd.Commands() {
100+
if sub.IsAdditionalHelpTopicCommand() {
101+
fmt.Fprintf(out, " %-*s %s\n", pad, sub.Name(), sub.Short)
102+
}
103+
}
104+
}
105+
106+
// Footer
107+
if cmd.HasAvailableSubCommands() {
108+
fmt.Fprintln(out)
109+
fmt.Fprintf(out, "Use \"%s [command] --help\" for more information about a command.\n", cmd.CommandPath())
110+
}
111+
}
112+
113+
// subcommandPadding returns the column width needed to align descriptions
114+
// in the Available Commands block.
115+
func subcommandPadding(cmd *cobra.Command) int {
116+
maxLen := 11 // Cobra's default minimum
117+
for _, sub := range cmd.Commands() {
118+
if (sub.IsAvailableCommand() || sub.IsAdditionalHelpTopicCommand()) && len(sub.Name()) > maxLen {
119+
maxLen = len(sub.Name())
120+
}
121+
}
122+
return maxLen
123+
}

cmd/apx/commands/link.go

Lines changed: 0 additions & 73 deletions
This file was deleted.

0 commit comments

Comments
 (0)