Skip to content

Commit 1d9b2a0

Browse files
committed
Style help output and avoid duplicate usage
1 parent 377d1d5 commit 1d9b2a0

File tree

3 files changed

+132
-28
lines changed

3 files changed

+132
-28
lines changed

cmd/auth.go

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,21 @@ func AuthCommand() *ffcli.Command {
1919
Name: "auth",
2020
ShortUsage: "asc auth <subcommand> [flags]",
2121
ShortHelp: "Manage App Store Connect API authentication.",
22-
LongHelp: "Manage App Store Connect API authentication.\n\nAuthentication is handled via App Store Connect API keys. Generate keys at:\nhttps://appstoreconnect.apple.com/access/integrations/api\n\nCredentials are stored in the system keychain when available, with a local config fallback.\n\nSubcommands:\n login Register and store API key\n logout Remove stored credentials\n status Show current authentication status",
23-
FlagSet: fs,
22+
LongHelp: `Manage App Store Connect API authentication.
23+
24+
Authentication is handled via App Store Connect API keys. Generate keys at:
25+
https://appstoreconnect.apple.com/access/integrations/api
26+
27+
Credentials are stored in the system keychain when available, with a local config fallback.`,
28+
FlagSet: fs,
29+
UsageFunc: DefaultUsageFunc,
2430
Subcommands: []*ffcli.Command{
2531
AuthLoginCommand(),
2632
AuthLogoutCommand(),
2733
AuthStatusCommand(),
2834
},
2935
Exec: func(ctx context.Context, args []string) error {
3036
if len(args) == 0 {
31-
fs.Usage()
3237
return flag.ErrHelp
3338
}
3439
return nil
@@ -58,26 +63,23 @@ Examples:
5863
asc auth login --name "MyKey" --key-id "ABC123" --issuer-id "DEF456" --private-key /path/to/AuthKey.p8
5964
6065
The private key file path is stored securely. The key content is never saved.`,
61-
FlagSet: fs,
66+
FlagSet: fs,
67+
UsageFunc: DefaultUsageFunc,
6268
Exec: func(ctx context.Context, args []string) error {
6369
if *name == "" {
6470
fmt.Fprintln(os.Stderr, "Error: --name is required")
65-
fs.Usage()
6671
return flag.ErrHelp
6772
}
6873
if *keyID == "" {
6974
fmt.Fprintln(os.Stderr, "Error: --key-id is required")
70-
fs.Usage()
7175
return flag.ErrHelp
7276
}
7377
if *issuerID == "" {
7478
fmt.Fprintln(os.Stderr, "Error: --issuer-id is required")
75-
fs.Usage()
7679
return flag.ErrHelp
7780
}
7881
if *keyPath == "" {
7982
fmt.Fprintln(os.Stderr, "Error: --private-key is required")
80-
fs.Usage()
8183
return flag.ErrHelp
8284
}
8385

@@ -111,7 +113,8 @@ func AuthLogoutCommand() *ffcli.Command {
111113
Examples:
112114
asc auth logout
113115
asc auth logout --all`,
114-
FlagSet: fs,
116+
FlagSet: fs,
117+
UsageFunc: DefaultUsageFunc,
115118
Exec: func(ctx context.Context, args []string) error {
116119
if *all {
117120
// Flag is accepted for future multi-key support.
@@ -141,7 +144,8 @@ Displays information about stored API keys and which one is currently active.
141144
142145
Examples:
143146
asc auth status`,
144-
FlagSet: fs,
147+
FlagSet: fs,
148+
UsageFunc: DefaultUsageFunc,
145149
Exec: func(ctx context.Context, args []string) error {
146150
credentials, err := auth.ListCredentials()
147151
if err != nil {

cmd/commands.go

Lines changed: 114 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,105 @@ import (
77
"net/url"
88
"os"
99
"strings"
10+
"text/tabwriter"
1011

1112
"github.com/peterbourgon/ff/v3/ffcli"
1213

1314
"github.com/rudrankriyam/App-Store-Connect-CLI/internal/asc"
1415
"github.com/rudrankriyam/App-Store-Connect-CLI/internal/auth"
1516
)
1617

18+
// ANSI escape codes for bold text
19+
var (
20+
bold = "\033[1m"
21+
reset = "\033[22m"
22+
)
23+
24+
// Bold returns the string wrapped in ANSI bold codes
25+
func Bold(s string) string {
26+
return bold + s + reset
27+
}
28+
29+
// DefaultUsageFunc returns a usage string with bold section headers
30+
func DefaultUsageFunc(c *ffcli.Command) string {
31+
var b strings.Builder
32+
33+
shortHelp := strings.TrimSpace(c.ShortHelp)
34+
longHelp := strings.TrimSpace(c.LongHelp)
35+
if shortHelp == "" && longHelp != "" {
36+
shortHelp = longHelp
37+
longHelp = ""
38+
}
39+
40+
// DESCRIPTION
41+
if shortHelp != "" {
42+
b.WriteString(Bold("DESCRIPTION"))
43+
b.WriteString("\n")
44+
b.WriteString(" ")
45+
b.WriteString(shortHelp)
46+
b.WriteString("\n\n")
47+
}
48+
49+
// USAGE / ShortUsage
50+
usage := strings.TrimSpace(c.ShortUsage)
51+
if usage == "" {
52+
usage = strings.TrimSpace(c.Name)
53+
}
54+
if usage != "" {
55+
b.WriteString(Bold("USAGE"))
56+
b.WriteString("\n")
57+
b.WriteString(" ")
58+
b.WriteString(usage)
59+
b.WriteString("\n\n")
60+
}
61+
62+
// LongHelp (additional description)
63+
if longHelp != "" {
64+
if shortHelp != "" && strings.HasPrefix(longHelp, shortHelp) {
65+
longHelp = strings.TrimSpace(strings.TrimPrefix(longHelp, shortHelp))
66+
}
67+
if longHelp != "" {
68+
b.WriteString(longHelp)
69+
b.WriteString("\n\n")
70+
}
71+
}
72+
73+
// SUBCOMMANDS
74+
if len(c.Subcommands) > 0 {
75+
b.WriteString(Bold("SUBCOMMANDS"))
76+
b.WriteString("\n")
77+
tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
78+
for _, sub := range c.Subcommands {
79+
fmt.Fprintf(tw, " %-12s %s\n", sub.Name, sub.ShortHelp)
80+
}
81+
tw.Flush()
82+
b.WriteString("\n")
83+
}
84+
85+
// FLAGS
86+
if c.FlagSet != nil {
87+
hasFlags := false
88+
c.FlagSet.VisitAll(func(*flag.Flag) {
89+
hasFlags = true
90+
})
91+
if hasFlags {
92+
b.WriteString(Bold("FLAGS"))
93+
b.WriteString("\n")
94+
tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
95+
c.FlagSet.VisitAll(func(f *flag.Flag) {
96+
def := f.DefValue
97+
if def == "" {
98+
def = "\"\""
99+
}
100+
fmt.Fprintf(tw, " --%-12s %s (default: %s)\n", f.Name, f.Usage, def)
101+
})
102+
tw.Flush()
103+
}
104+
}
105+
106+
return b.String()
107+
}
108+
17109
// Feedback command factory
18110
func FeedbackCommand() *ffcli.Command {
19111
fs := flag.NewFlagSet("feedback", flag.ExitOnError)
@@ -47,7 +139,8 @@ Examples:
47139
asc feedback --app "123456789" --device-model "iPhone15,3" --os-version "17.2"
48140
asc feedback --app "123456789" --sort -createdDate --limit 5 --json
49141
asc feedback --next "<links.next>" --json`,
50-
FlagSet: fs,
142+
FlagSet: fs,
143+
UsageFunc: DefaultUsageFunc,
51144
Exec: func(ctx context.Context, args []string) error {
52145
if *limit != 0 && (*limit < 1 || *limit > 200) {
53146
return fmt.Errorf("feedback: --limit must be between 1 and 200")
@@ -138,7 +231,8 @@ Examples:
138231
asc crashes --app "123456789" --device-model "iPhone15,3" --os-version "17.2"
139232
asc crashes --app "123456789" --sort -createdDate --limit 5 --json
140233
asc crashes --next "<links.next>" --json`,
141-
FlagSet: fs,
234+
FlagSet: fs,
235+
UsageFunc: DefaultUsageFunc,
142236
Exec: func(ctx context.Context, args []string) error {
143237
if *limit != 0 && (*limit < 1 || *limit > 200) {
144238
return fmt.Errorf("crashes: --limit must be between 1 and 200")
@@ -223,7 +317,8 @@ Examples:
223317
asc reviews --app "123456789" --stars 1 --territory US --json
224318
asc reviews --app "123456789" --sort -createdDate --limit 5 --json
225319
asc reviews --next "<links.next>" --json`,
226-
FlagSet: fs,
320+
FlagSet: fs,
321+
UsageFunc: DefaultUsageFunc,
227322
Exec: func(ctx context.Context, args []string) error {
228323
if *limit != 0 && (*limit < 1 || *limit > 200) {
229324
return fmt.Errorf("reviews: --limit must be between 1 and 200")
@@ -311,7 +406,8 @@ Examples:
311406
asc apps --sort name --json
312407
asc apps --output table
313408
asc apps --next "<links.next>" --json`,
314-
FlagSet: fs,
409+
FlagSet: fs,
410+
UsageFunc: DefaultUsageFunc,
315411
Exec: func(ctx context.Context, args []string) error {
316412
if *limit != 0 && (*limit < 1 || *limit > 200) {
317413
return fmt.Errorf("apps: --limit must be between 1 and 200")
@@ -379,7 +475,8 @@ with presigned URLs. The actual file upload must be done separately.
379475
Examples:
380476
asc builds upload --app "123456789" --ipa "path/to/app.ipa"
381477
asc builds upload --ipa "app.ipa" --version "1.0.0" --build-number "123"`,
382-
FlagSet: fs,
478+
FlagSet: fs,
479+
UsageFunc: DefaultUsageFunc,
383480
Exec: func(ctx context.Context, args []string) error {
384481
// Validate required flags
385482
resolvedAppID := resolveAppID(*appID)
@@ -502,18 +599,13 @@ func BuildsCommand() *ffcli.Command {
502599
ShortHelp: "Manage builds in App Store Connect.",
503600
LongHelp: `Manage builds in App Store Connect.
504601
505-
Subcommands:
506-
list List builds for an app
507-
info Show build details
508-
expire Expire a build for TestFlight
509-
upload Prepare a build upload
510-
511602
Examples:
512603
asc builds list --app "123456789"
513604
asc builds info --build "BUILD_ID"
514605
asc builds expire --build "BUILD_ID"
515606
asc builds upload --app "123456789" --ipa "app.ipa"`,
516-
FlagSet: fs,
607+
FlagSet: fs,
608+
UsageFunc: DefaultUsageFunc,
517609
Subcommands: []*ffcli.Command{
518610
listCmd,
519611
BuildsInfoCommand(),
@@ -551,7 +643,8 @@ Examples:
551643
asc builds list --app "123456789"
552644
asc builds list --app "123456789" --json
553645
asc builds list --app "123456789" --limit 10 --json`,
554-
FlagSet: fs,
646+
FlagSet: fs,
647+
UsageFunc: DefaultUsageFunc,
555648
Subcommands: []*ffcli.Command{
556649
BuildsInfoCommand(),
557650
BuildsExpireCommand(),
@@ -621,11 +714,11 @@ func BuildsInfoCommand() *ffcli.Command {
621714
622715
Examples:
623716
asc builds info --build "BUILD_ID" --json`,
624-
FlagSet: fs,
717+
FlagSet: fs,
718+
UsageFunc: DefaultUsageFunc,
625719
Exec: func(ctx context.Context, args []string) error {
626720
if strings.TrimSpace(*buildID) == "" {
627721
fmt.Fprintln(os.Stderr, "Error: --build is required")
628-
fs.Usage()
629722
return flag.ErrHelp
630723
}
631724

@@ -671,11 +764,11 @@ This action is irreversible for the specified build.
671764
672765
Examples:
673766
asc builds expire --build "BUILD_ID" --json`,
674-
FlagSet: fs,
767+
FlagSet: fs,
768+
UsageFunc: DefaultUsageFunc,
675769
Exec: func(ctx context.Context, args []string) error {
676770
if strings.TrimSpace(*buildID) == "" {
677771
fmt.Fprintln(os.Stderr, "Error: --build is required")
678-
fs.Usage()
679772
return flag.ErrHelp
680773
}
681774

@@ -724,7 +817,8 @@ a version for review on the App Store.
724817
Examples:
725818
asc submit --version "VERSION_ID" --confirm
726819
asc submit --version "VERSION_ID" --confirm --json`,
727-
FlagSet: fs,
820+
FlagSet: fs,
821+
UsageFunc: DefaultUsageFunc,
728822
Exec: func(ctx context.Context, args []string) error {
729823
// Validate required flags
730824
if *versionID == "" {
@@ -782,6 +876,7 @@ func VersionCommand(version string) *ffcli.Command {
782876
Name: "version",
783877
ShortUsage: "asc version",
784878
ShortHelp: "Print version information and exit.",
879+
UsageFunc: DefaultUsageFunc,
785880
Exec: func(ctx context.Context, args []string) error {
786881
fmt.Println(version)
787882
return nil
@@ -797,6 +892,7 @@ func RootCommand(version string) *ffcli.Command {
797892
ShortHelp: "A fast, AI-agent friendly CLI for App Store Connect.",
798893
LongHelp: "ASC is a lightweight CLI for App Store Connect. Built for developers and AI agents.",
799894
FlagSet: flag.NewFlagSet("asc", flag.ExitOnError),
895+
UsageFunc: DefaultUsageFunc,
800896
Subcommands: []*ffcli.Command{
801897
AuthCommand(),
802898
FeedbackCommand(),

main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"context"
5+
"errors"
56
"flag"
67
"fmt"
78
"log"
@@ -28,6 +29,9 @@ func main() {
2829
}
2930

3031
if err := root.Run(context.Background()); err != nil {
32+
if errors.Is(err, flag.ErrHelp) {
33+
os.Exit(1)
34+
}
3135
log.Fatalf("error executing command: %v\n", err)
3236
}
3337
}

0 commit comments

Comments
 (0)