From e4e8a8006a74fc33074d2fd3a528fb3476233c66 Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Fri, 10 Apr 2026 14:17:59 +0200 Subject: [PATCH 01/12] add runLegacy --- cmd/src/cmd.go | 65 ++++++++++++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/cmd/src/cmd.go b/cmd/src/cmd.go index 93d0accbe3..238f76bf84 100644 --- a/cmd/src/cmd.go +++ b/cmd/src/cmd.go @@ -73,39 +73,48 @@ func (c commander) run(flagSet *flag.FlagSet, cmdName, usageText string, args [] continue } - // Read global configuration now. - var err error - cfg, err = readConfig() + exitCode, err := runLegacy(cmd, flagSet) if err != nil { - log.Fatal("reading config: ", err) + log.Fatal(err) } + os.Exit(exitCode) - // Parse subcommand flags. - args := flagSet.Args()[1:] - if err := cmd.flagSet.Parse(args); err != nil { - fmt.Printf("Error parsing subcommand flags: %s\n", err) - panic(fmt.Sprintf("all registered commands should use flag.ExitOnError: error: %s", err)) - } + } + log.Printf("%s: unknown subcommand %q", cmdName, name) + log.Fatalf("Run '%s help' for usage.", cmdName) +} - // Execute the subcommand. - if err := cmd.handler(flagSet.Args()[1:]); err != nil { - if _, ok := err.(*cmderrors.UsageError); ok { - log.Printf("error: %s\n\n", err) - cmd.flagSet.SetOutput(os.Stderr) - flag.CommandLine.SetOutput(os.Stderr) - cmd.flagSet.Usage() - os.Exit(2) - } - if e, ok := err.(*cmderrors.ExitCodeError); ok { - if e.HasError() { - log.Println(e) - } - os.Exit(e.Code()) +func runLegacy(cmd *command, flagSet *flag.FlagSet) (int, error) { + // Read global configuration now. + var err error + cfg, err = readConfig() + if err != nil { + log.Fatal("reading config: ", err) + } + + // Parse subcommand flags. + args := flagSet.Args()[1:] + if err := cmd.flagSet.Parse(args); err != nil { + fmt.Printf("Error parsing subcommand flags: %s\n", err) + panic(fmt.Sprintf("all registered commands should use flag.ExitOnError: error: %s", err)) + } + + // Execute the subcommand. + if err := cmd.handler(flagSet.Args()[1:]); err != nil { + if _, ok := err.(*cmderrors.UsageError); ok { + log.Printf("error: %s\n\n", err) + cmd.flagSet.SetOutput(os.Stderr) + flag.CommandLine.SetOutput(os.Stderr) + cmd.flagSet.Usage() + return 2, nil + } + if e, ok := err.(*cmderrors.ExitCodeError); ok { + if e.HasError() { + log.Println(e) } - log.Fatal(err) + return e.Code(), nil } - os.Exit(0) + return 1, err } - log.Printf("%s: unknown subcommand %q", cmdName, name) - log.Fatalf("Run '%s help' for usage.", cmdName) + return 0, nil } From cb25b26aec367ffc4cbc31703564b1fdb97076f8 Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Fri, 10 Apr 2026 15:21:44 +0200 Subject: [PATCH 02/12] use go 1.26 --- go.mod | 3 ++- go.sum | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 274db0a7c8..75f66dfa62 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/sourcegraph/src-cli -go 1.25.8 +go 1.26 require ( cloud.google.com/go/storage v1.50.0 @@ -83,6 +83,7 @@ require ( github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/tliron/commonlog v0.2.19 // indirect github.com/tliron/kutil v0.3.27 // indirect + github.com/urfave/cli/v3 v3.8.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xlab/treeprint v1.2.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect diff --git a/go.sum b/go.sum index e85cdd9f5a..eda4224b98 100644 --- a/go.sum +++ b/go.sum @@ -370,6 +370,8 @@ github.com/tliron/glsp v0.2.2 h1:IKPfwpE8Lu8yB6Dayta+IyRMAbTVunudeauEgjXBt+c= github.com/tliron/glsp v0.2.2/go.mod h1:GMVWDNeODxHzmDPvYbYTCs7yHVaEATfYtXiYJ9w1nBg= github.com/tliron/kutil v0.3.27 h1:Wb0V5jdbTci6Let1tiGY741J/9FIynmV/pCsPDPsjcM= github.com/tliron/kutil v0.3.27/go.mod h1:AHeLNIFBSKBU39ELVHZdkw2f/ez2eKGAAGoxwBlhMi8= +github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI= +github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= From 81df0f23738e0ac78df5ad2c1c2ed32aa9c86f8b Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Fri, 10 Apr 2026 15:21:44 +0200 Subject: [PATCH 03/12] add util methods to work urfave/cli flags in api --- internal/api/flags.go | 13 +++++++++ internal/clicompat/api_flags.go | 51 +++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 internal/clicompat/api_flags.go diff --git a/internal/api/flags.go b/internal/api/flags.go index 32a255e371..be2397e818 100644 --- a/internal/api/flags.go +++ b/internal/api/flags.go @@ -48,6 +48,19 @@ func NewFlags(flagSet *flag.FlagSet) *Flags { } } +// NewFlagsFromValues instantiates a new Flags structure from explicit values. +// This is used by cli/v3 compatibility adapters that do not operate on a +// standard flag.FlagSet. +func NewFlagsFromValues(dump, getCurl, trace, insecureSkipVerify, userAgentTelemetry bool) *Flags { + return &Flags{ + dump: new(dump), + getCurl: new(getCurl), + trace: new(trace), + insecureSkipVerify: new(insecureSkipVerify), + userAgentTelemetry: new(userAgentTelemetry), + } +} + func defaultFlags() *Flags { telemetry := defaultUserAgentTelemetry() d := false diff --git a/internal/clicompat/api_flags.go b/internal/clicompat/api_flags.go new file mode 100644 index 0000000000..feff22a7e1 --- /dev/null +++ b/internal/clicompat/api_flags.go @@ -0,0 +1,51 @@ +package clicompat + +import ( + "os" + + "github.com/sourcegraph/src-cli/internal/api" + "github.com/urfave/cli/v3" +) + +// WithAPIFlags appends the standard API-related flags used by legacy src +// commands to a v3 command's flag set. +func WithAPIFlags(baseFlags ...cli.Flag) []cli.Flag { + var flagTable = []struct { + name string + value bool + text string + }{ + {"dump-requests", false, "Log GraphQL requests and responses to stdout"}, + {"get-curl", false, "Print the curl command for executing this query and exit (WARNING: includes printing your access token!)"}, + {"trace", false, "Log the trace ID for requests. See https://docs.sourcegraph.com/admin/observability/tracing"}, + {"insecure-skip-verify", false, "Skip validation of TLS certificates against trusted chains"}, + {"user-agent-telemetry", defaultAPIUserAgentTelemetry(), "Include the operating system and architecture in the User-Agent sent with requests to Sourcegraph"}, + } + + flags := append([]cli.Flag{}, baseFlags...) + for _, item := range flagTable { + flags = append(flags, &cli.BoolFlag{ + Name: item.name, + Value: item.value, + Usage: item.text, + }) + } + + return flags +} + +// APIFlagsFromContext reads the shared API-related flags from a cli/v3 command +// context into the existing api.Flags structure used by legacy command logic. +func APIFlagsFromContext(cmd *cli.Command) *api.Flags { + return api.NewFlagsFromValues( + cmd.Bool("dump-requests"), + cmd.Bool("get-curl"), + cmd.Bool("trace"), + cmd.Bool("insecure-skip-verify"), + cmd.Bool("user-agent-telemetry"), + ) +} + +func defaultAPIUserAgentTelemetry() bool { + return os.Getenv("SRC_DISABLE_USER_AGENT_TELEMETRY") == "" +} From d69e79c0ad4f3624b3259b04662f24cee673b82e Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Fri, 10 Apr 2026 15:21:44 +0200 Subject: [PATCH 04/12] tweak help template to match legacy help --- internal/clicompat/help.go | 54 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 internal/clicompat/help.go diff --git a/internal/clicompat/help.go b/internal/clicompat/help.go new file mode 100644 index 0000000000..7947e058ea --- /dev/null +++ b/internal/clicompat/help.go @@ -0,0 +1,54 @@ +package clicompat + +import "github.com/urfave/cli/v3" + +// LegacyCommandHelpTemplate formats leaf command help in a style closer to the +// existing flag.FlagSet-based help output. +const LegacyCommandHelpTemplate = `Usage of '{{.FullName}}': +{{range .VisibleFlags}} {{printf " -%s\n\t\t\t\t%s\n" .Name .Usage}}{{end}}{{if .Description}} + {{trim .Description}} +{{end}} +` + +// LegacyRootCommandHelpTemplate formats root command help while preserving a +// command's UsageText when it is provided. +const LegacyRootCommandHelpTemplate = `{{if .UsageText}}{{trim .UsageText}} +{{else}}Usage of '{{.FullName}}': +{{end}}{{if .VisibleFlags}}{{range .VisibleFlags}}{{println .}}{{end}}{{end}}{{if .VisibleCommands}} +{{range .VisibleCommands}}{{printf "\t%s\t%s\n" .Name .Usage}}{{end}}{{end}}{{if .Description}} +{{trim .Description}} +{{end}} +` + +// WithLegacyCommandHelp applies the shared legacy-style leaf command help +// template and returns the same command for inline construction. +func WithLegacyCommandHelp(cmd *cli.Command) *cli.Command { + if cmd == nil { + return nil + } + + cmd.CustomHelpTemplate = LegacyCommandHelpTemplate + return cmd +} + +// WithLegacyRootCommandHelp applies the shared legacy-style root help template +// and returns the same command for inline construction. +func WithLegacyRootCommandHelp(cmd *cli.Command) *cli.Command { + if cmd == nil { + return nil + } + + cmd.CustomRootCommandHelpTemplate = LegacyRootCommandHelpTemplate + return cmd +} + +// WithLegacyHelp applies both root and leaf legacy help templates. +func WithLegacyHelp(cmd *cli.Command) *cli.Command { + if cmd == nil { + return nil + } + + WithLegacyCommandHelp(cmd) + WithLegacyRootCommandHelp(cmd) + return cmd +} From 09b487bb3b9d616b6d8ed836a90c3aa9b80fc0e5 Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Fri, 10 Apr 2026 15:21:44 +0200 Subject: [PATCH 05/12] split command runner into legacy and migrated --- cmd/src/cmd.go | 68 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/cmd/src/cmd.go b/cmd/src/cmd.go index 238f76bf84..09c1922f64 100644 --- a/cmd/src/cmd.go +++ b/cmd/src/cmd.go @@ -1,15 +1,25 @@ package main import ( + "context" "flag" "fmt" "log" "os" "slices" + "sort" + "github.com/sourcegraph/src-cli/internal/clicompat" "github.com/sourcegraph/src-cli/internal/cmderrors" + "github.com/urfave/cli/v3" + + "github.com/sourcegraph/sourcegraph/lib/errors" ) +var MigratedCommands = map[string]*cli.Command{ + "version": versionCommandv2, +} + // command is a subcommand handler and its flag set. type command struct { // flagSet is the flag set for the command. @@ -68,12 +78,26 @@ func (c commander) run(flagSet *flag.FlagSet, cmdName, usageText string, args [] // Find the subcommand to execute. name := flagSet.Arg(0) + for _, cmd := range c { - if !cmd.matches(name) { + _, isMigratedCmd := MigratedCommands[name] + if !isMigratedCmd && !cmd.matches(name) { continue } + // Read global configuration now. + var err error + cfg, err = readConfig() + if err != nil { + log.Fatal("reading config: ", err) + } + + var exitCode int - exitCode, err := runLegacy(cmd, flagSet) + if isMigratedCmd { + exitCode, err = runMigrated(flagSet) + } else { + exitCode, err = runLegacy(cmd, flagSet) + } if err != nil { log.Fatal(err) } @@ -84,14 +108,42 @@ func (c commander) run(flagSet *flag.FlagSet, cmdName, usageText string, args [] log.Fatalf("Run '%s help' for usage.", cmdName) } -func runLegacy(cmd *command, flagSet *flag.FlagSet) (int, error) { - // Read global configuration now. - var err error - cfg, err = readConfig() - if err != nil { - log.Fatal("reading config: ", err) +// migratedRootCommand constructs a root 'src' command and adds +// MigratedCommands as subcommands to it +func migratedRootCommand() *cli.Command { + names := make([]string, 0, len(MigratedCommands)) + for name := range MigratedCommands { + names = append(names, name) } + sort.Strings(names) + commands := make([]*cli.Command, 0, len(names)) + for _, name := range names { + commands = append(commands, MigratedCommands[name]) + } + + return clicompat.WithLegacyRootCommandHelp(&cli.Command{ + Name: "src", + HideVersion: true, + Commands: commands, + }) +} + +// runMigrated runs the command within urfave/cli framework +func runMigrated(flagSet *flag.FlagSet) (int, error) { + ctx := context.Background() + args := append([]string{"src"}, flagSet.Args()...) + + err := migratedRootCommand().Run(ctx, args) + var exitErr cli.ExitCoder + if errors.AsInterface(err, &exitErr) { + return exitErr.ExitCode(), err + } + return 0, err +} + +// runLegacy runs the command using the original commander framework +func runLegacy(cmd *command, flagSet *flag.FlagSet) (int, error) { // Parse subcommand flags. args := flagSet.Args()[1:] if err := cmd.flagSet.Parse(args); err != nil { From f4ff6caccf1aebc11d253cf341c6f4c8dd6acf91 Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Fri, 10 Apr 2026 15:21:44 +0200 Subject: [PATCH 06/12] migrate version to use urface cli --- cmd/src/version.go | 81 +++++++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 36 deletions(-) diff --git a/cmd/src/version.go b/cmd/src/version.go index 024ab0600e..c5874eea19 100644 --- a/cmd/src/version.go +++ b/cmd/src/version.go @@ -3,63 +3,72 @@ package main import ( "context" "encoding/json" - "flag" "fmt" "io" "net/http" + "os" "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/src-cli/internal/api" + "github.com/sourcegraph/src-cli/internal/clicompat" "github.com/sourcegraph/src-cli/internal/version" + + "github.com/urfave/cli/v3" ) -func init() { - usage := ` -Examples: +const versionExamples = `Examples: Get the src-cli version and the Sourcegraph instance's recommended version: $ src version ` - flagSet := flag.NewFlagSet("version", flag.ExitOnError) - - var ( - clientOnly = flagSet.Bool("client-only", false, "If true, only the client version will be printed.") - apiFlags = api.NewFlags(flagSet) - ) - - handler := func(args []string) error { - fmt.Printf("Current version: %s\n", version.BuildTag) - if clientOnly != nil && *clientOnly { - return nil +var versionCommandv2 = clicompat.WithLegacyCommandHelp(&cli.Command{ + Name: "version", + Usage: "display and compare the src-cli version against the recommended version for your instance", + UsageText: "src version [options]", + Description: ` +` + versionExamples, + Flags: clicompat.WithAPIFlags( + &cli.BoolFlag{ + Name: "client-only", + Usage: "If true, only the client version will be printed.", + }, + ), + HideVersion: true, + Action: func(ctx context.Context, c *cli.Command) error { + args := VersionArgs{ + Client: cfg.apiClient(clicompat.APIFlagsFromContext(c), os.Stdout), + ClientOnly: c.Bool("client-only"), } + return versionHandler(args) + }, +}) + +type VersionArgs struct { + ClientOnly bool + Client api.Client + Output io.Writer +} - client := cfg.apiClient(apiFlags, flagSet.Output()) - recommendedVersion, err := getRecommendedVersion(context.Background(), client) - if err != nil { - return errors.Wrap(err, "failed to get recommended version for Sourcegraph deployment") - } - if recommendedVersion == "" { - fmt.Println("Recommended version: ") - fmt.Println("This Sourcegraph instance does not support this feature.") - return nil - } - fmt.Printf("Recommended version: %s or later\n", recommendedVersion) +func versionHandler(args VersionArgs) error { + fmt.Printf("Current version: %s\n", version.BuildTag) + if args.ClientOnly { return nil } - // Register the command. - commands = append(commands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src %s':\n", flagSet.Name()) - flagSet.PrintDefaults() - fmt.Println(usage) - }, - }) + recommendedVersion, err := getRecommendedVersion(context.Background(), args.Client) + if err != nil { + return errors.Wrap(err, "failed to get recommended version for Sourcegraph deployment") + } + if recommendedVersion == "" { + fmt.Println("Recommended version: ") + fmt.Println("This Sourcegraph instance does not support this feature.") + return nil + } + fmt.Printf("Recommended version: %s or later\n", recommendedVersion) + return nil } func getRecommendedVersion(ctx context.Context, client api.Client) (string, error) { From 302e9ff830ac24be5bd32b9346e2dc11fa7691ab Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Mon, 13 Apr 2026 14:24:31 +0200 Subject: [PATCH 07/12] add onUsageError func and rename funcs to be 'Wrap*' --- internal/clicompat/errors.go | 16 ++++++++++++++++ internal/clicompat/help.go | 18 ++++++++++-------- 2 files changed, 26 insertions(+), 8 deletions(-) create mode 100644 internal/clicompat/errors.go diff --git a/internal/clicompat/errors.go b/internal/clicompat/errors.go new file mode 100644 index 0000000000..f06886b4bf --- /dev/null +++ b/internal/clicompat/errors.go @@ -0,0 +1,16 @@ +package clicompat + +import ( + "context" + "fmt" + "os" + + "github.com/sourcegraph/src-cli/internal/cmderrors" + "github.com/urfave/cli/v3" +) + +func OnUsageError(ctx context.Context, cmd *cli.Command, err error, isSubCommand bool) error { + fmt.Fprintf(os.Stderr, "error: %s\n", err.Error()) + cli.DefaultPrintHelp(os.Stderr, cmd.CustomHelpTemplate, cmd) + return cmderrors.Usage(err.Error()) +} diff --git a/internal/clicompat/help.go b/internal/clicompat/help.go index 7947e058ea..f458ad6fbb 100644 --- a/internal/clicompat/help.go +++ b/internal/clicompat/help.go @@ -20,25 +20,25 @@ const LegacyRootCommandHelpTemplate = `{{if .UsageText}}{{trim .UsageText}} {{end}} ` -// WithLegacyCommandHelp applies the shared legacy-style leaf command help -// template and returns the same command for inline construction. -func WithLegacyCommandHelp(cmd *cli.Command) *cli.Command { +// Wrap sets common options on a sub commands to ensure consistency for help and error handling +func Wrap(cmd *cli.Command) *cli.Command { if cmd == nil { return nil } cmd.CustomHelpTemplate = LegacyCommandHelpTemplate + cmd.OnUsageError = OnUsageError return cmd } -// WithLegacyRootCommandHelp applies the shared legacy-style root help template -// and returns the same command for inline construction. -func WithLegacyRootCommandHelp(cmd *cli.Command) *cli.Command { +// WrapRoot sets common options on a root command to ensure consistency for help and error handling +func WrapRoot(cmd *cli.Command) *cli.Command { if cmd == nil { return nil } cmd.CustomRootCommandHelpTemplate = LegacyRootCommandHelpTemplate + cmd.OnUsageError = OnUsageError return cmd } @@ -48,7 +48,9 @@ func WithLegacyHelp(cmd *cli.Command) *cli.Command { return nil } - WithLegacyCommandHelp(cmd) - WithLegacyRootCommandHelp(cmd) + cmd.CustomHelpTemplate = LegacyCommandHelpTemplate + cmd.CustomRootCommandHelpTemplate = LegacyRootCommandHelpTemplate + cmd.OnUsageError = OnUsageError + return cmd } From 8cac955ccc6b709ea0e105808417acbb6a220bab Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Mon, 13 Apr 2026 14:25:00 +0200 Subject: [PATCH 08/12] use Wrap from clicompat instead --- cmd/src/cmd.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cmd/src/cmd.go b/cmd/src/cmd.go index 09c1922f64..0c3b39b438 100644 --- a/cmd/src/cmd.go +++ b/cmd/src/cmd.go @@ -2,6 +2,7 @@ package main import ( "context" + stderrors "errors" "flag" "fmt" "log" @@ -122,7 +123,7 @@ func migratedRootCommand() *cli.Command { commands = append(commands, MigratedCommands[name]) } - return clicompat.WithLegacyRootCommandHelp(&cli.Command{ + return clicompat.WrapRoot(&cli.Command{ Name: "src", HideVersion: true, Commands: commands, @@ -135,6 +136,9 @@ func runMigrated(flagSet *flag.FlagSet) (int, error) { args := append([]string{"src"}, flagSet.Args()...) err := migratedRootCommand().Run(ctx, args) + if _, ok := stderrors.AsType[*cmderrors.UsageError](err); ok { + return 2, nil + } var exitErr cli.ExitCoder if errors.AsInterface(err, &exitErr) { return exitErr.ExitCode(), err From 9d0554e90db665e623f8e36df0e96a66d245b050 Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Mon, 13 Apr 2026 14:25:00 +0200 Subject: [PATCH 09/12] remove init --- cmd/src/version.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cmd/src/version.go b/cmd/src/version.go index c5874eea19..e1917291b6 100644 --- a/cmd/src/version.go +++ b/cmd/src/version.go @@ -24,10 +24,11 @@ const versionExamples = `Examples: $ src version ` -var versionCommandv2 = clicompat.WithLegacyCommandHelp(&cli.Command{ - Name: "version", - Usage: "display and compare the src-cli version against the recommended version for your instance", - UsageText: "src version [options]", +var versionCommandv2 = clicompat.Wrap(&cli.Command{ + Name: "version", + Usage: "display and compare the src-cli version against the recommended version for your instance", + UsageText: "src version [options]", + OnUsageError: clicompat.OnUsageError, Description: ` ` + versionExamples, Flags: clicompat.WithAPIFlags( From a8a63361ab747c9ebab08f0b215ddbedd3309ad7 Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Mon, 13 Apr 2026 14:44:13 +0200 Subject: [PATCH 10/12] update comments and method names of clicompat api flags --- cmd/src/version.go | 2 +- internal/clicompat/api_flags.go | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/cmd/src/version.go b/cmd/src/version.go index e1917291b6..dc45fa9fb2 100644 --- a/cmd/src/version.go +++ b/cmd/src/version.go @@ -40,7 +40,7 @@ var versionCommandv2 = clicompat.Wrap(&cli.Command{ HideVersion: true, Action: func(ctx context.Context, c *cli.Command) error { args := VersionArgs{ - Client: cfg.apiClient(clicompat.APIFlagsFromContext(c), os.Stdout), + Client: cfg.apiClient(clicompat.APIFlagsFromCmd(c), os.Stdout), ClientOnly: c.Bool("client-only"), } return versionHandler(args) diff --git a/internal/clicompat/api_flags.go b/internal/clicompat/api_flags.go index feff22a7e1..736d9997cb 100644 --- a/internal/clicompat/api_flags.go +++ b/internal/clicompat/api_flags.go @@ -8,7 +8,6 @@ import ( ) // WithAPIFlags appends the standard API-related flags used by legacy src -// commands to a v3 command's flag set. func WithAPIFlags(baseFlags ...cli.Flag) []cli.Flag { var flagTable = []struct { name string @@ -34,9 +33,8 @@ func WithAPIFlags(baseFlags ...cli.Flag) []cli.Flag { return flags } -// APIFlagsFromContext reads the shared API-related flags from a cli/v3 command -// context into the existing api.Flags structure used by legacy command logic. -func APIFlagsFromContext(cmd *cli.Command) *api.Flags { +// APIFlagsFromCmd reads the shared API-related flags from a command into api.Flags +func APIFlagsFromCmd(cmd *cli.Command) *api.Flags { return api.NewFlagsFromValues( cmd.Bool("dump-requests"), cmd.Bool("get-curl"), From 27fb40d796aeaa8504b8c7de12dbb8f1f66b6603 Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Mon, 13 Apr 2026 15:31:47 +0200 Subject: [PATCH 11/12] move runMigrated to be in main and work with os.Args --- cmd/src/cmd.go | 91 +--------------------------- cmd/src/main.go | 13 +++- cmd/src/run_migration_compat.go | 102 ++++++++++++++++++++++++++++++++ cmd/src/version.go | 2 +- 4 files changed, 118 insertions(+), 90 deletions(-) create mode 100644 cmd/src/run_migration_compat.go diff --git a/cmd/src/cmd.go b/cmd/src/cmd.go index 0c3b39b438..9182dc57ea 100644 --- a/cmd/src/cmd.go +++ b/cmd/src/cmd.go @@ -1,26 +1,13 @@ package main import ( - "context" - stderrors "errors" "flag" "fmt" "log" "os" "slices" - "sort" - - "github.com/sourcegraph/src-cli/internal/clicompat" - "github.com/sourcegraph/src-cli/internal/cmderrors" - "github.com/urfave/cli/v3" - - "github.com/sourcegraph/sourcegraph/lib/errors" ) -var MigratedCommands = map[string]*cli.Command{ - "version": versionCommandv2, -} - // command is a subcommand handler and its flag set. type command struct { // flagSet is the flag set for the command. @@ -80,9 +67,9 @@ func (c commander) run(flagSet *flag.FlagSet, cmdName, usageText string, args [] // Find the subcommand to execute. name := flagSet.Arg(0) + // Command is legacy, so lets execute the old way for _, cmd := range c { - _, isMigratedCmd := MigratedCommands[name] - if !isMigratedCmd && !cmd.matches(name) { + if !cmd.matches(name) { continue } // Read global configuration now. @@ -92,13 +79,7 @@ func (c commander) run(flagSet *flag.FlagSet, cmdName, usageText string, args [] log.Fatal("reading config: ", err) } - var exitCode int - - if isMigratedCmd { - exitCode, err = runMigrated(flagSet) - } else { - exitCode, err = runLegacy(cmd, flagSet) - } + exitCode, err := runLegacy(cmd, flagSet) if err != nil { log.Fatal(err) } @@ -108,69 +89,3 @@ func (c commander) run(flagSet *flag.FlagSet, cmdName, usageText string, args [] log.Printf("%s: unknown subcommand %q", cmdName, name) log.Fatalf("Run '%s help' for usage.", cmdName) } - -// migratedRootCommand constructs a root 'src' command and adds -// MigratedCommands as subcommands to it -func migratedRootCommand() *cli.Command { - names := make([]string, 0, len(MigratedCommands)) - for name := range MigratedCommands { - names = append(names, name) - } - sort.Strings(names) - - commands := make([]*cli.Command, 0, len(names)) - for _, name := range names { - commands = append(commands, MigratedCommands[name]) - } - - return clicompat.WrapRoot(&cli.Command{ - Name: "src", - HideVersion: true, - Commands: commands, - }) -} - -// runMigrated runs the command within urfave/cli framework -func runMigrated(flagSet *flag.FlagSet) (int, error) { - ctx := context.Background() - args := append([]string{"src"}, flagSet.Args()...) - - err := migratedRootCommand().Run(ctx, args) - if _, ok := stderrors.AsType[*cmderrors.UsageError](err); ok { - return 2, nil - } - var exitErr cli.ExitCoder - if errors.AsInterface(err, &exitErr) { - return exitErr.ExitCode(), err - } - return 0, err -} - -// runLegacy runs the command using the original commander framework -func runLegacy(cmd *command, flagSet *flag.FlagSet) (int, error) { - // Parse subcommand flags. - args := flagSet.Args()[1:] - if err := cmd.flagSet.Parse(args); err != nil { - fmt.Printf("Error parsing subcommand flags: %s\n", err) - panic(fmt.Sprintf("all registered commands should use flag.ExitOnError: error: %s", err)) - } - - // Execute the subcommand. - if err := cmd.handler(flagSet.Args()[1:]); err != nil { - if _, ok := err.(*cmderrors.UsageError); ok { - log.Printf("error: %s\n\n", err) - cmd.flagSet.SetOutput(os.Stderr) - flag.CommandLine.SetOutput(os.Stderr) - cmd.flagSet.Usage() - return 2, nil - } - if e, ok := err.(*cmderrors.ExitCodeError); ok { - if e.HasError() { - log.Println(e) - } - return e.Code(), nil - } - return 1, err - } - return 0, nil -} diff --git a/cmd/src/main.go b/cmd/src/main.go index 93be07c4bf..975593cdd0 100644 --- a/cmd/src/main.go +++ b/cmd/src/main.go @@ -93,7 +93,18 @@ func main() { log.SetFlags(0) log.SetPrefix("") - commands.run(flag.CommandLine, "src", usageText, normalizeDashHelp(os.Args[1:])) + err, exitCode, ranMigratedCmd := maybeRunMigratedCommand() + if ranMigratedCmd { + if err != nil { + log.Println(err) + } + os.Exit(exitCode) + } + + // if we didn't run a migrated command, then lets try running the legacy version + if !ranMigratedCmd { + commands.run(flag.CommandLine, "src", usageText, normalizeDashHelp(os.Args[1:])) + } } // normalizeDashHelp converts --help to -help since Go's flag parser only supports single dash. diff --git a/cmd/src/run_migration_compat.go b/cmd/src/run_migration_compat.go new file mode 100644 index 0000000000..da3b1f50c6 --- /dev/null +++ b/cmd/src/run_migration_compat.go @@ -0,0 +1,102 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log" + "os" + "sort" + + "github.com/sourcegraph/src-cli/internal/clicompat" + "github.com/sourcegraph/src-cli/internal/cmderrors" + "github.com/urfave/cli/v3" + + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +var migratedCommands = map[string]*cli.Command{ + "version": versionCommand, +} + +func maybeRunMigratedCommand() (err error, exitCode int, isMigrated bool) { + // need to figure out if a migrated command has been requested + flag.Parse() + subCommand := flag.CommandLine.Arg(0) + _, isMigrated = migratedCommands[subCommand] + if !isMigrated { + return + } + cfg, err = readConfig() + if err != nil { + log.Fatal("reading config: ", err) + } + + exitCode, err = runMigrated() + return +} + +// migratedRootCommand constructs a root 'src' command and adds +// MigratedCommands as subcommands to it +func migratedRootCommand() *cli.Command { + names := make([]string, 0, len(migratedCommands)) + for name := range migratedCommands { + names = append(names, name) + } + sort.Strings(names) + + commands := make([]*cli.Command, 0, len(names)) + for _, name := range names { + commands = append(commands, migratedCommands[name]) + } + + return clicompat.WrapRoot(&cli.Command{ + Name: "src", + HideVersion: true, + Commands: commands, + }) +} + +// runMigrated runs the command within urfave/cli framework +func runMigrated() (int, error) { + ctx := context.Background() + + err := migratedRootCommand().Run(ctx, os.Args) + if errors.HasType[*cmderrors.UsageError](err) { + return 2, nil + } + var exitErr cli.ExitCoder + if errors.AsInterface(err, &exitErr) { + return exitErr.ExitCode(), err + } + return 0, err +} + +// runLegacy runs the command using the original commander framework +func runLegacy(cmd *command, flagSet *flag.FlagSet) (int, error) { + // Parse subcommand flags. + args := flagSet.Args()[1:] + if err := cmd.flagSet.Parse(args); err != nil { + fmt.Printf("Error parsing subcommand flags: %s\n", err) + panic(fmt.Sprintf("all registered commands should use flag.ExitOnError: error: %s", err)) + } + + // Execute the subcommand. + if err := cmd.handler(flagSet.Args()[1:]); err != nil { + if _, ok := err.(*cmderrors.UsageError); ok { + log.Printf("error: %s\n\n", err) + cmd.flagSet.SetOutput(os.Stderr) + flag.CommandLine.SetOutput(os.Stderr) + cmd.flagSet.Usage() + return 2, nil + } + if e, ok := err.(*cmderrors.ExitCodeError); ok { + if e.HasError() { + log.Println(e) + } + return e.Code(), nil + } + return 1, err + } + return 0, nil +} diff --git a/cmd/src/version.go b/cmd/src/version.go index dc45fa9fb2..6186159368 100644 --- a/cmd/src/version.go +++ b/cmd/src/version.go @@ -24,7 +24,7 @@ const versionExamples = `Examples: $ src version ` -var versionCommandv2 = clicompat.Wrap(&cli.Command{ +var versionCommand = clicompat.Wrap(&cli.Command{ Name: "version", Usage: "display and compare the src-cli version against the recommended version for your instance", UsageText: "src version [options]", From 349b951a84e5dd29293b98d3d29b2d644078e320 Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Tue, 14 Apr 2026 17:49:24 +0200 Subject: [PATCH 12/12] fix lint --- cmd/src/main.go | 2 +- cmd/src/run_migration_compat.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/src/main.go b/cmd/src/main.go index 975593cdd0..1bc131c0eb 100644 --- a/cmd/src/main.go +++ b/cmd/src/main.go @@ -93,7 +93,7 @@ func main() { log.SetFlags(0) log.SetPrefix("") - err, exitCode, ranMigratedCmd := maybeRunMigratedCommand() + ranMigratedCmd, exitCode, err := maybeRunMigratedCommand() if ranMigratedCmd { if err != nil { log.Println(err) diff --git a/cmd/src/run_migration_compat.go b/cmd/src/run_migration_compat.go index da3b1f50c6..c50f2cd34a 100644 --- a/cmd/src/run_migration_compat.go +++ b/cmd/src/run_migration_compat.go @@ -19,7 +19,7 @@ var migratedCommands = map[string]*cli.Command{ "version": versionCommand, } -func maybeRunMigratedCommand() (err error, exitCode int, isMigrated bool) { +func maybeRunMigratedCommand() (isMigrated bool, exitCode int, err error) { // need to figure out if a migrated command has been requested flag.Parse() subCommand := flag.CommandLine.Arg(0)