From 491acd434b0f5dd521fd84e9ec78f6cf70190e97 Mon Sep 17 00:00:00 2001 From: bahrmichael Date: Wed, 15 Apr 2026 10:39:21 +0200 Subject: [PATCH 01/16] src abc command --- cmd/src/abc.go | 37 ++++++++++ cmd/src/abc_variables.go | 37 ++++++++++ cmd/src/abc_variables_set.go | 113 ++++++++++++++++++++++++++++++ cmd/src/abc_variables_set_test.go | 64 +++++++++++++++++ cmd/src/main.go | 1 + 5 files changed, 252 insertions(+) create mode 100644 cmd/src/abc.go create mode 100644 cmd/src/abc_variables.go create mode 100644 cmd/src/abc_variables_set.go create mode 100644 cmd/src/abc_variables_set_test.go diff --git a/cmd/src/abc.go b/cmd/src/abc.go new file mode 100644 index 0000000000..5d5f5c9038 --- /dev/null +++ b/cmd/src/abc.go @@ -0,0 +1,37 @@ +package main + +import ( + "flag" + "fmt" +) + +var abcCommands commander + +func init() { + usage := `'src abc' is a tool that manages agentic batch changes on a Sourcegraph instance. + +Usage: + + src abc command [command options] + +The commands are: + + variables manage workflow instance variables + +Use "src abc [command] -h" for more information about a command. +` + + flagSet := flag.NewFlagSet("abc", flag.ExitOnError) + handler := func(args []string) error { + abcCommands.run(flagSet, "src abc", usage, args) + return nil + } + + commands = append(commands, &command{ + flagSet: flagSet, + handler: handler, + usageFunc: func() { + fmt.Println(usage) + }, + }) +} diff --git a/cmd/src/abc_variables.go b/cmd/src/abc_variables.go new file mode 100644 index 0000000000..4d919e78fa --- /dev/null +++ b/cmd/src/abc_variables.go @@ -0,0 +1,37 @@ +package main + +import ( + "flag" + "fmt" +) + +var abcVariablesCommands commander + +func init() { + usage := `'src abc variables' is a tool that manages workflow instance variables on a Sourcegraph instance. + +Usage: + + src abc variables command [command options] + +The commands are: + + set set or remove a workflow instance variable + +Use "src abc variables [command] -h" for more information about a command. +` + + flagSet := flag.NewFlagSet("variables", flag.ExitOnError) + handler := func(args []string) error { + abcVariablesCommands.run(flagSet, "src abc variables", usage, args) + return nil + } + + abcCommands = append(abcCommands, &command{ + flagSet: flagSet, + handler: handler, + usageFunc: func() { + fmt.Println(usage) + }, + }) +} diff --git a/cmd/src/abc_variables_set.go b/cmd/src/abc_variables_set.go new file mode 100644 index 0000000000..bcf06a1b1c --- /dev/null +++ b/cmd/src/abc_variables_set.go @@ -0,0 +1,113 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + + "github.com/sourcegraph/src-cli/internal/api" + "github.com/sourcegraph/src-cli/internal/cmderrors" +) + +const updateABCWorkflowInstanceVariablesMutation = `mutation UpdateAgenticWorkflowInstanceVariables( + $instanceID: ID!, + $variables: [AgenticWorkflowInstanceVariableInput!]!, +) { + updateAgenticWorkflowInstanceVariables(instanceID: $instanceID, variables: $variables) { + id + } +}` + +func init() { + usage := ` +Examples: + + Set a string variable on a workflow instance: + + $ src abc variables set QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== prompt "tighten the review criteria" + + Remove a variable by setting it to null: + + $ src abc variables set QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== approval null + + Set a structured JSON value: + + $ src abc variables set QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== checkpoints '[1,2,3]' + +Values are interpreted as JSON literals when valid. Otherwise they are sent as plain strings. + ` + + flagSet := flag.NewFlagSet("set", flag.ExitOnError) + usageFunc := func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src abc variables %s':\n", flagSet.Name()) + flagSet.PrintDefaults() + fmt.Println(usage) + } + apiFlags := api.NewFlags(flagSet) + + handler := func(args []string) error { + if err := flagSet.Parse(args); err != nil { + return err + } + + if flagSet.NArg() != 3 { + return cmderrors.Usage("must provide an instance ID, variable name, and variable value") + } + + instanceID := flagSet.Arg(0) + key := flagSet.Arg(1) + value, remove, err := marshalABCVariableValue(flagSet.Arg(2)) + if err != nil { + return err + } + + client := cfg.apiClient(apiFlags, flagSet.Output()) + var result struct { + UpdateAgenticWorkflowInstanceVariables struct { + ID string `json:"id"` + } `json:"updateAgenticWorkflowInstanceVariables"` + } + if ok, err := client.NewRequest(updateABCWorkflowInstanceVariablesMutation, map[string]any{ + "instanceID": instanceID, + "variables": []map[string]string{{ + "key": key, + "value": value, + }}, + }).Do(context.Background(), &result); err != nil || !ok { + return err + } + + if apiFlags.GetCurl() { + return nil + } + + if remove { + fmt.Printf("Removed variable %q from workflow instance %q.\n", key, instanceID) + return nil + } + + fmt.Printf("Set variable %q on workflow instance %q.\n", key, instanceID) + return nil + } + + abcVariablesCommands = append(abcVariablesCommands, &command{ + flagSet: flagSet, + handler: handler, + usageFunc: usageFunc, + }) +} + +func marshalABCVariableValue(raw string) (value string, remove bool, err error) { + var parsed any + if err := json.Unmarshal([]byte(raw), &parsed); err != nil { + parsed = raw + } + + encoded, err := json.Marshal(parsed) + if err != nil { + return "", false, err + } + + return string(encoded), parsed == nil, nil +} diff --git a/cmd/src/abc_variables_set_test.go b/cmd/src/abc_variables_set_test.go new file mode 100644 index 0000000000..adbca6c714 --- /dev/null +++ b/cmd/src/abc_variables_set_test.go @@ -0,0 +1,64 @@ +package main + +import "testing" + +func TestMarshalABCVariableValue(t *testing.T) { + tests := []struct { + name string + raw string + wantValue string + wantRemove bool + }{ + { + name: "plain string", + raw: "hello", + wantValue: `"hello"`, + wantRemove: false, + }, + { + name: "number literal", + raw: "42", + wantValue: `42`, + wantRemove: false, + }, + { + name: "boolean literal", + raw: "true", + wantValue: `true`, + wantRemove: false, + }, + { + name: "null removes variable", + raw: "null", + wantValue: `null`, + wantRemove: true, + }, + { + name: "quoted null stays a string", + raw: "\"null\"", + wantValue: "\"null\"", + wantRemove: false, + }, + { + name: "object literal", + raw: "{\"retries\":3,\"notify\":true}", + wantValue: "{\"notify\":true,\"retries\":3}", + wantRemove: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + gotValue, gotRemove, err := marshalABCVariableValue(test.raw) + if err != nil { + t.Fatalf("marshalABCVariableValue returned error: %s", err) + } + if gotValue != test.wantValue { + t.Fatalf("value = %q, want %q", gotValue, test.wantValue) + } + if gotRemove != test.wantRemove { + t.Fatalf("remove = %v, want %v", gotRemove, test.wantRemove) + } + }) + } +} diff --git a/cmd/src/main.go b/cmd/src/main.go index 93be07c4bf..b9bd73be8e 100644 --- a/cmd/src/main.go +++ b/cmd/src/main.go @@ -50,6 +50,7 @@ The options are: The commands are: + abc manages agentic batch changes auth authentication helper commands api interacts with the Sourcegraph GraphQL API batch manages batch changes From 858595c3c59877ef60732564c438d3f9d8fc406b Mon Sep 17 00:00:00 2001 From: bahrmichael Date: Wed, 15 Apr 2026 10:44:37 +0200 Subject: [PATCH 02/16] remove unnecessary test --- cmd/src/abc_variables_set_test.go | 64 ------------------------------- 1 file changed, 64 deletions(-) delete mode 100644 cmd/src/abc_variables_set_test.go diff --git a/cmd/src/abc_variables_set_test.go b/cmd/src/abc_variables_set_test.go deleted file mode 100644 index adbca6c714..0000000000 --- a/cmd/src/abc_variables_set_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package main - -import "testing" - -func TestMarshalABCVariableValue(t *testing.T) { - tests := []struct { - name string - raw string - wantValue string - wantRemove bool - }{ - { - name: "plain string", - raw: "hello", - wantValue: `"hello"`, - wantRemove: false, - }, - { - name: "number literal", - raw: "42", - wantValue: `42`, - wantRemove: false, - }, - { - name: "boolean literal", - raw: "true", - wantValue: `true`, - wantRemove: false, - }, - { - name: "null removes variable", - raw: "null", - wantValue: `null`, - wantRemove: true, - }, - { - name: "quoted null stays a string", - raw: "\"null\"", - wantValue: "\"null\"", - wantRemove: false, - }, - { - name: "object literal", - raw: "{\"retries\":3,\"notify\":true}", - wantValue: "{\"notify\":true,\"retries\":3}", - wantRemove: false, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - gotValue, gotRemove, err := marshalABCVariableValue(test.raw) - if err != nil { - t.Fatalf("marshalABCVariableValue returned error: %s", err) - } - if gotValue != test.wantValue { - t.Fatalf("value = %q, want %q", gotValue, test.wantValue) - } - if gotRemove != test.wantRemove { - t.Fatalf("remove = %v, want %v", gotRemove, test.wantRemove) - } - }) - } -} From abe6bafbc4b5b5501a5003b7231ecc0ea06f52bd Mon Sep 17 00:00:00 2001 From: bahrmichael Date: Wed, 15 Apr 2026 11:08:17 +0200 Subject: [PATCH 03/16] self review --- cmd/src/abc_variables_set.go | 14 ++++++++----- cmd/src/abc_variables_set_test.go | 33 +++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 cmd/src/abc_variables_set_test.go diff --git a/cmd/src/abc_variables_set.go b/cmd/src/abc_variables_set.go index bcf06a1b1c..8f0d376e68 100644 --- a/cmd/src/abc_variables_set.go +++ b/cmd/src/abc_variables_set.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "context" "encoding/json" "flag" @@ -99,15 +100,18 @@ Values are interpreted as JSON literals when valid. Otherwise they are sent as p } func marshalABCVariableValue(raw string) (value string, remove bool, err error) { - var parsed any - if err := json.Unmarshal([]byte(raw), &parsed); err != nil { - parsed = raw + // Try to compact valid JSON literals first. This allows us to send null to delete values, as well as raw numbers. + // If we that doesn't work for the given value, fall back to string encoding. + var compact bytes.Buffer + if err := json.Compact(&compact, []byte(raw)); err == nil { + value := compact.String() + return value, value == "null", nil } - encoded, err := json.Marshal(parsed) + encoded, err := json.Marshal(raw) if err != nil { return "", false, err } - return string(encoded), parsed == nil, nil + return string(encoded), false, nil } diff --git a/cmd/src/abc_variables_set_test.go b/cmd/src/abc_variables_set_test.go new file mode 100644 index 0000000000..4495dcf471 --- /dev/null +++ b/cmd/src/abc_variables_set_test.go @@ -0,0 +1,33 @@ +package main + +import "testing" + +func TestMarshalABCVariableValuePreservesLargeIntegerLiteral(t *testing.T) { + t.Parallel() + + value, remove, err := marshalABCVariableValue("9007199254740993") + if err != nil { + t.Fatalf("marshalABCVariableValue returned error: %v", err) + } + if remove { + t.Fatal("marshalABCVariableValue unexpectedly marked value for removal") + } + if value != "9007199254740993" { + t.Fatalf("marshalABCVariableValue = %q, want %q", value, "9007199254740993") + } +} + +func TestMarshalABCVariableValueTreatsNullAsRemoval(t *testing.T) { + t.Parallel() + + value, remove, err := marshalABCVariableValue("null") + if err != nil { + t.Fatalf("marshalABCVariableValue returned error: %v", err) + } + if !remove { + t.Fatal("marshalABCVariableValue did not mark null for removal") + } + if value != "null" { + t.Fatalf("marshalABCVariableValue = %q, want %q", value, "null") + } +} From 179c121bb940d8bd0b704a1adf9c93f55fc8eb1a Mon Sep 17 00:00:00 2001 From: bahrmichael Date: Wed, 15 Apr 2026 11:10:05 +0200 Subject: [PATCH 04/16] comment --- cmd/src/abc_variables_set_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/src/abc_variables_set_test.go b/cmd/src/abc_variables_set_test.go index 4495dcf471..16f3ae4fa8 100644 --- a/cmd/src/abc_variables_set_test.go +++ b/cmd/src/abc_variables_set_test.go @@ -2,6 +2,8 @@ package main import "testing" +// If we were to do a json marshalling roundtrip, it may break large integer literals. +// This test is here to demonstrate that the compaction approach is working well. func TestMarshalABCVariableValuePreservesLargeIntegerLiteral(t *testing.T) { t.Parallel() From 1c11dcd3ddb29b73990e52f0cd13e65565114e00 Mon Sep 17 00:00:00 2001 From: bahrmichael Date: Wed, 15 Apr 2026 11:48:11 +0200 Subject: [PATCH 05/16] semantics update --- cmd/src/abc.go | 35 ++++++-- cmd/src/abc_variables.go | 40 +++++++--- cmd/src/abc_variables_delete.go | 64 +++++++++++++++ cmd/src/abc_variables_set.go | 127 +++++++++++++++++++++++------- cmd/src/abc_variables_set_test.go | 70 +++++++++++++++- cmd/src/cmd.go | 53 +++++++++++++ 6 files changed, 341 insertions(+), 48 deletions(-) create mode 100644 cmd/src/abc_variables_delete.go diff --git a/cmd/src/abc.go b/cmd/src/abc.go index 5d5f5c9038..e186d85dfb 100644 --- a/cmd/src/abc.go +++ b/cmd/src/abc.go @@ -3,6 +3,8 @@ package main import ( "flag" "fmt" + + "github.com/sourcegraph/src-cli/internal/cmderrors" ) var abcCommands commander @@ -12,26 +14,43 @@ func init() { Usage: - src abc command [command options] + src abc command [command options] The commands are: variables manage workflow instance variables -Use "src abc [command] -h" for more information about a command. +Use "src abc [command] -h" for more information about a command. ` flagSet := flag.NewFlagSet("abc", flag.ExitOnError) + usageFunc := func() { + fmt.Println(usage) + } + flagSet.Usage = usageFunc handler := func(args []string) error { - abcCommands.run(flagSet, "src abc", usage, args) + if err := flagSet.Parse(args); err != nil { + return err + } + + if flagSet.NArg() == 0 || flagSet.Arg(0) == "help" { + flagSet.SetOutput(flag.CommandLine.Output()) + flagSet.Usage() + return nil + } + + if flagSet.NArg() < 2 { + return cmderrors.Usage("must provide an instance ID and subcommand") + } + + instanceID := flagSet.Arg(0) + abcCommands.runWithPrefixArgs("src abc ", []string{instanceID}, flagSet.Args()[1:]) return nil } commands = append(commands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: func() { - fmt.Println(usage) - }, + flagSet: flagSet, + handler: handler, + usageFunc: usageFunc, }) } diff --git a/cmd/src/abc_variables.go b/cmd/src/abc_variables.go index 4d919e78fa..bc3a053a7d 100644 --- a/cmd/src/abc_variables.go +++ b/cmd/src/abc_variables.go @@ -3,35 +3,55 @@ package main import ( "flag" "fmt" + + "github.com/sourcegraph/src-cli/internal/cmderrors" ) var abcVariablesCommands commander func init() { - usage := `'src abc variables' is a tool that manages workflow instance variables on a Sourcegraph instance. + usage := `'src abc variables' is a tool that manages workflow instance variables on a Sourcegraph instance. Usage: - src abc variables command [command options] + src abc variables command [command options] The commands are: - set set or remove a workflow instance variable + set set workflow instance variables + delete delete a workflow instance variable -Use "src abc variables [command] -h" for more information about a command. +Use "src abc variables [command] -h" for more information about a command. ` flagSet := flag.NewFlagSet("variables", flag.ExitOnError) + usageFunc := func() { + fmt.Println(usage) + } + flagSet.Usage = usageFunc handler := func(args []string) error { - abcVariablesCommands.run(flagSet, "src abc variables", usage, args) + if len(args) == 0 { + return cmderrors.Usage("must provide an instance ID") + } + + instanceID := args[0] + if err := flagSet.Parse(args[1:]); err != nil { + return err + } + + if flagSet.NArg() == 0 || flagSet.Arg(0) == "help" { + flagSet.SetOutput(flag.CommandLine.Output()) + flagSet.Usage() + return nil + } + + abcVariablesCommands.runWithPrefixArgs("src abc variables", []string{instanceID}, flagSet.Args()) return nil } abcCommands = append(abcCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: func() { - fmt.Println(usage) - }, + flagSet: flagSet, + handler: handler, + usageFunc: usageFunc, }) } diff --git a/cmd/src/abc_variables_delete.go b/cmd/src/abc_variables_delete.go new file mode 100644 index 0000000000..0e9bbea99a --- /dev/null +++ b/cmd/src/abc_variables_delete.go @@ -0,0 +1,64 @@ +package main + +import ( + "context" + "flag" + "fmt" + + "github.com/sourcegraph/src-cli/internal/api" + "github.com/sourcegraph/src-cli/internal/cmderrors" +) + +func init() { + usage := ` +Examples: + + Delete a variable from a workflow instance: + + $ src abc QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== variables delete approval + ` + + flagSet := flag.NewFlagSet("delete", flag.ExitOnError) + usageFunc := func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src abc variables %s':\n", flagSet.Name()) + flagSet.PrintDefaults() + fmt.Println(usage) + } + apiFlags := api.NewFlags(flagSet) + + handler := func(args []string) error { + if len(args) == 0 { + return cmderrors.Usage("must provide an instance ID") + } + + instanceID := args[0] + if err := flagSet.Parse(args[1:]); err != nil { + return err + } + if flagSet.NArg() != 1 { + return cmderrors.Usage("must provide exactly one variable name") + } + + key := flagSet.Arg(0) + client := cfg.apiClient(apiFlags, flagSet.Output()) + if err := updateABCWorkflowInstanceVariables(context.Background(), client, instanceID, []map[string]string{{ + "key": key, + "value": "null", + }}); err != nil { + return err + } + + if apiFlags.GetCurl() { + return nil + } + + fmt.Printf("Removed variable %q from workflow instance %q.\n", key, instanceID) + return nil + } + + abcVariablesCommands = append(abcVariablesCommands, &command{ + flagSet: flagSet, + handler: handler, + usageFunc: usageFunc, + }) +} diff --git a/cmd/src/abc_variables_set.go b/cmd/src/abc_variables_set.go index 8f0d376e68..45f822544e 100644 --- a/cmd/src/abc_variables_set.go +++ b/cmd/src/abc_variables_set.go @@ -6,6 +6,7 @@ import ( "encoding/json" "flag" "fmt" + "strings" "github.com/sourcegraph/src-cli/internal/api" "github.com/sourcegraph/src-cli/internal/cmderrors" @@ -26,56 +27,55 @@ Examples: Set a string variable on a workflow instance: - $ src abc variables set QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== prompt "tighten the review criteria" + $ src abc QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== variables set prompt="tighten the review criteria" - Remove a variable by setting it to null: + Set multiple variables in one request: - $ src abc variables set QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== approval null + $ src abc QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== variables set --var prompt="tighten the review criteria" --var checkpoints='[1,2,3]' Set a structured JSON value: - $ src abc variables set QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== checkpoints '[1,2,3]' + $ src abc QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== variables set checkpoints='[1,2,3]' Values are interpreted as JSON literals when valid. Otherwise they are sent as plain strings. ` flagSet := flag.NewFlagSet("set", flag.ExitOnError) + var variableArgs abcVariableArgs + flagSet.Var(&variableArgs, "var", "Variable assignment in = form. Repeat to set multiple variables.") usageFunc := func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src abc variables %s':\n", flagSet.Name()) + fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src abc variables %s':\n", flagSet.Name()) flagSet.PrintDefaults() fmt.Println(usage) } apiFlags := api.NewFlags(flagSet) handler := func(args []string) error { - if err := flagSet.Parse(args); err != nil { - return err + if len(args) == 0 { + return cmderrors.Usage("must provide an instance ID") } - if flagSet.NArg() != 3 { - return cmderrors.Usage("must provide an instance ID, variable name, and variable value") + instanceID := args[0] + variableArgs = nil + if err := flagSet.Parse(args[1:]); err != nil { + return err } - instanceID := flagSet.Arg(0) - key := flagSet.Arg(1) - value, remove, err := marshalABCVariableValue(flagSet.Arg(2)) + variables, err := parseABCVariables(flagSet.Args(), variableArgs) if err != nil { return err } - client := cfg.apiClient(apiFlags, flagSet.Output()) - var result struct { - UpdateAgenticWorkflowInstanceVariables struct { - ID string `json:"id"` - } `json:"updateAgenticWorkflowInstanceVariables"` + graphqlVariables := make([]map[string]string, 0, len(variables)) + for _, variable := range variables { + graphqlVariables = append(graphqlVariables, map[string]string{ + "key": variable.Key, + "value": variable.Value, + }) } - if ok, err := client.NewRequest(updateABCWorkflowInstanceVariablesMutation, map[string]any{ - "instanceID": instanceID, - "variables": []map[string]string{{ - "key": key, - "value": value, - }}, - }).Do(context.Background(), &result); err != nil || !ok { + + client := cfg.apiClient(apiFlags, flagSet.Output()) + if err := updateABCWorkflowInstanceVariables(context.Background(), client, instanceID, graphqlVariables); err != nil { return err } @@ -83,12 +83,12 @@ Values are interpreted as JSON literals when valid. Otherwise they are sent as p return nil } - if remove { - fmt.Printf("Removed variable %q from workflow instance %q.\n", key, instanceID) + if len(variables) == 1 { + fmt.Printf("Set variable %q on workflow instance %q.\n", variables[0].Key, instanceID) return nil } - fmt.Printf("Set variable %q on workflow instance %q.\n", key, instanceID) + fmt.Printf("Updated %d variables on workflow instance %q.\n", len(variables), instanceID) return nil } @@ -99,9 +99,78 @@ Values are interpreted as JSON literals when valid. Otherwise they are sent as p }) } +type abcVariableArgs []string + +func (a *abcVariableArgs) String() string { + return strings.Join(*a, ",") +} + +func (a *abcVariableArgs) Set(value string) error { + *a = append(*a, value) + return nil +} + +type abcVariable struct { + Key string + Value string +} + +func parseABCVariables(positional []string, flagged abcVariableArgs) ([]abcVariable, error) { + rawVariables := append([]string{}, positional...) + rawVariables = append(rawVariables, flagged...) + if len(rawVariables) == 0 { + return nil, cmderrors.Usage("must provide at least one variable assignment") + } + + variables := make([]abcVariable, 0, len(rawVariables)) + for _, rawVariable := range rawVariables { + variable, err := parseABCVariable(rawVariable) + if err != nil { + return nil, err + } + variables = append(variables, variable) + } + + return variables, nil +} + +func parseABCVariable(raw string) (abcVariable, error) { + name, rawValue, ok := strings.Cut(raw, "=") + if !ok || name == "" { + return abcVariable{}, cmderrors.Usagef("invalid variable assignment %q: must be in = form", raw) + } + + value, remove, err := marshalABCVariableValue(rawValue) + if err != nil { + return abcVariable{}, err + } + if remove { + return abcVariable{}, cmderrors.Usagef("invalid variable assignment %q: use 'src abc variables delete %s' to remove a variable", raw, name) + } + + return abcVariable{Key: name, Value: value}, nil +} + +func updateABCWorkflowInstanceVariables(ctx context.Context, client api.Client, instanceID string, variables []map[string]string) error { + var result struct { + UpdateAgenticWorkflowInstanceVariables struct { + ID string `json:"id"` + } `json:"updateAgenticWorkflowInstanceVariables"` + } + if ok, err := client.NewRequest(updateABCWorkflowInstanceVariablesMutation, map[string]any{ + "instanceID": instanceID, + "variables": variables, + }).Do(ctx, &result); err != nil || !ok { + return err + } + + return nil +} + func marshalABCVariableValue(raw string) (value string, remove bool, err error) { - // Try to compact valid JSON literals first. This allows us to send null to delete values, as well as raw numbers. - // If we that doesn't work for the given value, fall back to string encoding. + // Try to compact valid JSON literals first so numbers, arrays, and objects are sent unchanged. + // A bare null is detected separately so the CLI can require the explicit delete command. + // If compacting doesn't work for the given value, fall back to string encoding. var compact bytes.Buffer if err := json.Compact(&compact, []byte(raw)); err == nil { value := compact.String() diff --git a/cmd/src/abc_variables_set_test.go b/cmd/src/abc_variables_set_test.go index 16f3ae4fa8..28ffff5658 100644 --- a/cmd/src/abc_variables_set_test.go +++ b/cmd/src/abc_variables_set_test.go @@ -1,6 +1,10 @@ package main -import "testing" +import ( + "testing" + + "github.com/sourcegraph/src-cli/internal/cmderrors" +) // If we were to do a json marshalling roundtrip, it may break large integer literals. // This test is here to demonstrate that the compaction approach is working well. @@ -33,3 +37,67 @@ func TestMarshalABCVariableValueTreatsNullAsRemoval(t *testing.T) { t.Fatalf("marshalABCVariableValue = %q, want %q", value, "null") } } + +func TestParseABCVariables(t *testing.T) { + t.Parallel() + + variables, err := parseABCVariables( + []string{"prompt=tighten the review criteria", `title="null"`}, + abcVariableArgs{"checkpoints=[1,2,3]"}, + ) + if err != nil { + t.Fatalf("parseABCVariables returned error: %v", err) + } + + if len(variables) != 3 { + t.Fatalf("len(variables) = %d, want 3", len(variables)) + } + + if variables[0].Key != "prompt" || variables[0].Value != `"tighten the review criteria"` { + t.Fatalf("variables[0] = %#v, want prompt string variable", variables[0]) + } + + if variables[1].Key != "title" || variables[1].Value != `"null"` { + t.Fatalf("variables[1] = %#v, want quoted null string", variables[1]) + } + + if variables[2].Key != "checkpoints" || variables[2].Value != "[1,2,3]" { + t.Fatalf("variables[2] = %#v, want compact JSON array", variables[2]) + } +} + +func TestParseABCVariablesRequiresAssignments(t *testing.T) { + t.Parallel() + + _, err := parseABCVariables(nil, nil) + if err == nil { + t.Fatal("parseABCVariables returned nil error, want usage error") + } + if _, ok := err.(*cmderrors.UsageError); !ok { + t.Fatalf("parseABCVariables error = %T, want *cmderrors.UsageError", err) + } +} + +func TestParseABCVariableRequiresNameValueFormat(t *testing.T) { + t.Parallel() + + _, err := parseABCVariable("missing-separator") + if err == nil { + t.Fatal("parseABCVariable returned nil error, want usage error") + } + if _, ok := err.(*cmderrors.UsageError); !ok { + t.Fatalf("parseABCVariable error = %T, want *cmderrors.UsageError", err) + } +} + +func TestParseABCVariableRejectsNullLiteral(t *testing.T) { + t.Parallel() + + _, err := parseABCVariable("approval=null") + if err == nil { + t.Fatal("parseABCVariable returned nil error, want usage error") + } + if _, ok := err.(*cmderrors.UsageError); !ok { + t.Fatalf("parseABCVariable error = %T, want *cmderrors.UsageError", err) + } +} diff --git a/cmd/src/cmd.go b/cmd/src/cmd.go index 93d0accbe3..e10454fd11 100644 --- a/cmd/src/cmd.go +++ b/cmd/src/cmd.go @@ -109,3 +109,56 @@ 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) } + +// runWithPrefixArgs runs a command tree where one or more leading arguments must +// be forwarded to the matched subcommand handler before its own arguments. +func (c commander) runWithPrefixArgs(cmdName string, prefixArgs, args []string) { + // Configure default usage funcs for commands. + for _, cmd := range c { + if cmd.usageFunc != nil { + cmd.flagSet.Usage = cmd.usageFunc + continue + } + cmd.flagSet.Usage = func() { + _, _ = fmt.Fprintf(flag.CommandLine.Output(), "Usage of '%s %s':\n", cmdName, cmd.flagSet.Name()) + cmd.flagSet.PrintDefaults() + } + } + + // Find the subcommand to execute. + name := args[0] + for _, cmd := range c { + if !cmd.matches(name) { + continue + } + + // Read global configuration now. + var err error + cfg, err = readConfig() + if err != nil { + log.Fatal("reading config: ", err) + } + + // Execute the subcommand. + if err := cmd.handler(append(slices.Clone(prefixArgs), 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()) + } + log.Fatal(err) + } + os.Exit(0) + } + + log.Printf("%s: unknown subcommand %q", cmdName, name) + log.Fatalf("Run '%s help' for usage.", cmdName) +} From 54879fb87cfac9c01178347f63901dbed6b9ac69 Mon Sep 17 00:00:00 2001 From: bahrmichael Date: Wed, 15 Apr 2026 11:59:14 +0200 Subject: [PATCH 06/16] workflow instance id naming --- cmd/src/abc.go | 8 ++++---- cmd/src/abc_variables.go | 10 +++++----- cmd/src/abc_variables_delete.go | 4 ++-- cmd/src/abc_variables_set.go | 6 +++--- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/cmd/src/abc.go b/cmd/src/abc.go index e186d85dfb..a857d40ab1 100644 --- a/cmd/src/abc.go +++ b/cmd/src/abc.go @@ -14,13 +14,13 @@ func init() { Usage: - src abc command [command options] + src abc command [command options] The commands are: variables manage workflow instance variables -Use "src abc [command] -h" for more information about a command. +Use "src abc [command] -h" for more information about a command. ` flagSet := flag.NewFlagSet("abc", flag.ExitOnError) @@ -40,11 +40,11 @@ Use "src abc [command] -h" for more information about a command. } if flagSet.NArg() < 2 { - return cmderrors.Usage("must provide an instance ID and subcommand") + return cmderrors.Usage("must provide a workflow instance ID and subcommand") } instanceID := flagSet.Arg(0) - abcCommands.runWithPrefixArgs("src abc ", []string{instanceID}, flagSet.Args()[1:]) + abcCommands.runWithPrefixArgs("src abc ", []string{instanceID}, flagSet.Args()[1:]) return nil } diff --git a/cmd/src/abc_variables.go b/cmd/src/abc_variables.go index bc3a053a7d..ced46fd1cc 100644 --- a/cmd/src/abc_variables.go +++ b/cmd/src/abc_variables.go @@ -10,18 +10,18 @@ import ( var abcVariablesCommands commander func init() { - usage := `'src abc variables' is a tool that manages workflow instance variables on a Sourcegraph instance. + usage := `'src abc variables' is a tool that manages workflow instance variables on a Sourcegraph instance. Usage: - src abc variables command [command options] + src abc variables command [command options] The commands are: set set workflow instance variables delete delete a workflow instance variable -Use "src abc variables [command] -h" for more information about a command. +Use "src abc variables [command] -h" for more information about a command. ` flagSet := flag.NewFlagSet("variables", flag.ExitOnError) @@ -31,7 +31,7 @@ Use "src abc variables [command] -h" for more information about a flagSet.Usage = usageFunc handler := func(args []string) error { if len(args) == 0 { - return cmderrors.Usage("must provide an instance ID") + return cmderrors.Usage("must provide a workflow instance ID") } instanceID := args[0] @@ -45,7 +45,7 @@ Use "src abc variables [command] -h" for more information about a return nil } - abcVariablesCommands.runWithPrefixArgs("src abc variables", []string{instanceID}, flagSet.Args()) + abcVariablesCommands.runWithPrefixArgs("src abc variables", []string{instanceID}, flagSet.Args()) return nil } diff --git a/cmd/src/abc_variables_delete.go b/cmd/src/abc_variables_delete.go index 0e9bbea99a..ab52fa927f 100644 --- a/cmd/src/abc_variables_delete.go +++ b/cmd/src/abc_variables_delete.go @@ -20,7 +20,7 @@ Examples: flagSet := flag.NewFlagSet("delete", flag.ExitOnError) usageFunc := func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src abc variables %s':\n", flagSet.Name()) + fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src abc variables %s':\n", flagSet.Name()) flagSet.PrintDefaults() fmt.Println(usage) } @@ -28,7 +28,7 @@ Examples: handler := func(args []string) error { if len(args) == 0 { - return cmderrors.Usage("must provide an instance ID") + return cmderrors.Usage("must provide a workflow instance ID") } instanceID := args[0] diff --git a/cmd/src/abc_variables_set.go b/cmd/src/abc_variables_set.go index 45f822544e..be47a13034 100644 --- a/cmd/src/abc_variables_set.go +++ b/cmd/src/abc_variables_set.go @@ -44,7 +44,7 @@ Values are interpreted as JSON literals when valid. Otherwise they are sent as p var variableArgs abcVariableArgs flagSet.Var(&variableArgs, "var", "Variable assignment in = form. Repeat to set multiple variables.") usageFunc := func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src abc variables %s':\n", flagSet.Name()) + fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src abc variables %s':\n", flagSet.Name()) flagSet.PrintDefaults() fmt.Println(usage) } @@ -52,7 +52,7 @@ Values are interpreted as JSON literals when valid. Otherwise they are sent as p handler := func(args []string) error { if len(args) == 0 { - return cmderrors.Usage("must provide an instance ID") + return cmderrors.Usage("must provide a workflow instance ID") } instanceID := args[0] @@ -145,7 +145,7 @@ func parseABCVariable(raw string) (abcVariable, error) { return abcVariable{}, err } if remove { - return abcVariable{}, cmderrors.Usagef("invalid variable assignment %q: use 'src abc variables delete %s' to remove a variable", raw, name) + return abcVariable{}, cmderrors.Usagef("invalid variable assignment %q: use 'src abc variables delete %s' to remove a variable", raw, name) } return abcVariable{Key: name, Value: value}, nil From ef3c68985d5dcbb9512332cd3e05a30501d469bf Mon Sep 17 00:00:00 2001 From: Michael Bahr <1830132+bahrmichael@users.noreply.github.com> Date: Wed, 15 Apr 2026 12:01:58 +0200 Subject: [PATCH 07/16] Apply suggestions from code review Co-authored-by: Michael Bahr <1830132+bahrmichael@users.noreply.github.com> --- cmd/src/abc.go | 2 +- cmd/src/abc_variables.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/src/abc.go b/cmd/src/abc.go index a857d40ab1..936d6df0d5 100644 --- a/cmd/src/abc.go +++ b/cmd/src/abc.go @@ -10,7 +10,7 @@ import ( var abcCommands commander func init() { - usage := `'src abc' is a tool that manages agentic batch changes on a Sourcegraph instance. + usage := `'src abc' is a tool that manages agentic batch changes. Usage: diff --git a/cmd/src/abc_variables.go b/cmd/src/abc_variables.go index ced46fd1cc..a21db8c21b 100644 --- a/cmd/src/abc_variables.go +++ b/cmd/src/abc_variables.go @@ -10,7 +10,7 @@ import ( var abcVariablesCommands commander func init() { - usage := `'src abc variables' is a tool that manages workflow instance variables on a Sourcegraph instance. + usage := `'src abc variables' is a tool that manages workflow instance variables. Usage: @@ -18,8 +18,8 @@ Usage: The commands are: - set set workflow instance variables - delete delete a workflow instance variable + set set workflow instance variables + delete delete workflow instance variables Use "src abc variables [command] -h" for more information about a command. ` From adae0488e2dd6ceb10117e039b1897bd366823de Mon Sep 17 00:00:00 2001 From: bahrmichael Date: Wed, 15 Apr 2026 12:03:37 +0200 Subject: [PATCH 08/16] delete support multiple vars --- cmd/src/abc_variables_delete.go | 50 +++++++++++++++++++++++----- cmd/src/abc_variables_delete_test.go | 50 ++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 8 deletions(-) create mode 100644 cmd/src/abc_variables_delete_test.go diff --git a/cmd/src/abc_variables_delete.go b/cmd/src/abc_variables_delete.go index ab52fa927f..e56a240380 100644 --- a/cmd/src/abc_variables_delete.go +++ b/cmd/src/abc_variables_delete.go @@ -16,9 +16,15 @@ Examples: Delete a variable from a workflow instance: $ src abc QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== variables delete approval + + Delete multiple variables in one request: + + $ src abc QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== variables delete --var approval --var checkpoints ` flagSet := flag.NewFlagSet("delete", flag.ExitOnError) + var variableArgs abcVariableArgs + flagSet.Var(&variableArgs, "var", "Variable name to delete. Repeat to delete multiple variables.") usageFunc := func() { fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src abc variables %s':\n", flagSet.Name()) flagSet.PrintDefaults() @@ -32,19 +38,26 @@ Examples: } instanceID := args[0] + variableArgs = nil if err := flagSet.Parse(args[1:]); err != nil { return err } - if flagSet.NArg() != 1 { - return cmderrors.Usage("must provide exactly one variable name") + + variableNames, err := parseABCVariableNames(flagSet.Args(), variableArgs) + if err != nil { + return err + } + + variables := make([]map[string]string, 0, len(variableNames)) + for _, key := range variableNames { + variables = append(variables, map[string]string{ + "key": key, + "value": "null", + }) } - key := flagSet.Arg(0) client := cfg.apiClient(apiFlags, flagSet.Output()) - if err := updateABCWorkflowInstanceVariables(context.Background(), client, instanceID, []map[string]string{{ - "key": key, - "value": "null", - }}); err != nil { + if err := updateABCWorkflowInstanceVariables(context.Background(), client, instanceID, variables); err != nil { return err } @@ -52,7 +65,12 @@ Examples: return nil } - fmt.Printf("Removed variable %q from workflow instance %q.\n", key, instanceID) + if len(variableNames) == 1 { + fmt.Printf("Removed variable %q from workflow instance %q.\n", variableNames[0], instanceID) + return nil + } + + fmt.Printf("Removed %d variables from workflow instance %q.\n", len(variableNames), instanceID) return nil } @@ -62,3 +80,19 @@ Examples: usageFunc: usageFunc, }) } + +func parseABCVariableNames(positional []string, flagged abcVariableArgs) ([]string, error) { + variableNames := append([]string{}, positional...) + variableNames = append(variableNames, flagged...) + if len(variableNames) == 0 { + return nil, cmderrors.Usage("must provide at least one variable name") + } + + for _, name := range variableNames { + if name == "" { + return nil, cmderrors.Usage("variable names must not be empty") + } + } + + return variableNames, nil +} diff --git a/cmd/src/abc_variables_delete_test.go b/cmd/src/abc_variables_delete_test.go new file mode 100644 index 0000000000..c2d6c67415 --- /dev/null +++ b/cmd/src/abc_variables_delete_test.go @@ -0,0 +1,50 @@ +package main + +import ( + "testing" + + "github.com/sourcegraph/src-cli/internal/cmderrors" +) + +func TestParseABCVariableNames(t *testing.T) { + t.Parallel() + + variableNames, err := parseABCVariableNames( + []string{"approval"}, + abcVariableArgs{"checkpoints", "prompt"}, + ) + if err != nil { + t.Fatalf("parseABCVariableNames returned error: %v", err) + } + + if len(variableNames) != 3 { + t.Fatalf("len(variableNames) = %d, want 3", len(variableNames)) + } + if variableNames[0] != "approval" || variableNames[1] != "checkpoints" || variableNames[2] != "prompt" { + t.Fatalf("variableNames = %#v, want [approval checkpoints prompt]", variableNames) + } +} + +func TestParseABCVariableNamesRequiresAtLeastOneName(t *testing.T) { + t.Parallel() + + _, err := parseABCVariableNames(nil, nil) + if err == nil { + t.Fatal("parseABCVariableNames returned nil error, want usage error") + } + if _, ok := err.(*cmderrors.UsageError); !ok { + t.Fatalf("parseABCVariableNames error = %T, want *cmderrors.UsageError", err) + } +} + +func TestParseABCVariableNamesRejectsEmptyNames(t *testing.T) { + t.Parallel() + + _, err := parseABCVariableNames([]string{"approval", ""}, nil) + if err == nil { + t.Fatal("parseABCVariableNames returned nil error, want usage error") + } + if _, ok := err.(*cmderrors.UsageError); !ok { + t.Fatalf("parseABCVariableNames error = %T, want *cmderrors.UsageError", err) + } +} From 238cf6f3f089ece0e7a93446c744e3c97e5ebe74 Mon Sep 17 00:00:00 2001 From: bahrmichael Date: Wed, 15 Apr 2026 12:11:28 +0200 Subject: [PATCH 09/16] wording updates --- cmd/src/abc_variables_delete.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/cmd/src/abc_variables_delete.go b/cmd/src/abc_variables_delete.go index e56a240380..b4122cd5a0 100644 --- a/cmd/src/abc_variables_delete.go +++ b/cmd/src/abc_variables_delete.go @@ -24,7 +24,7 @@ Examples: flagSet := flag.NewFlagSet("delete", flag.ExitOnError) var variableArgs abcVariableArgs - flagSet.Var(&variableArgs, "var", "Variable name to delete. Repeat to delete multiple variables.") + flagSet.Var(&variableArgs, "var", "Variable name to delete. Repeat for multiple names.") usageFunc := func() { fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src abc variables %s':\n", flagSet.Name()) flagSet.PrintDefaults() @@ -65,12 +65,7 @@ Examples: return nil } - if len(variableNames) == 1 { - fmt.Printf("Removed variable %q from workflow instance %q.\n", variableNames[0], instanceID) - return nil - } - - fmt.Printf("Removed %d variables from workflow instance %q.\n", len(variableNames), instanceID) + fmt.Printf("Removed variables %q from workflow instance %q.\n", variableNames, instanceID) return nil } From 3bcf8180aa6d08ae555ac4bee052e96d4a2539ce Mon Sep 17 00:00:00 2001 From: bahrmichael Date: Wed, 15 Apr 2026 12:14:58 +0200 Subject: [PATCH 10/16] remove unnecessary tests --- cmd/src/abc_variables_delete_test.go | 31 ++++------------------------ 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/cmd/src/abc_variables_delete_test.go b/cmd/src/abc_variables_delete_test.go index c2d6c67415..a0adc12b6f 100644 --- a/cmd/src/abc_variables_delete_test.go +++ b/cmd/src/abc_variables_delete_test.go @@ -3,7 +3,7 @@ package main import ( "testing" - "github.com/sourcegraph/src-cli/internal/cmderrors" + "github.com/google/go-cmp/cmp" ) func TestParseABCVariableNames(t *testing.T) { @@ -20,31 +20,8 @@ func TestParseABCVariableNames(t *testing.T) { if len(variableNames) != 3 { t.Fatalf("len(variableNames) = %d, want 3", len(variableNames)) } - if variableNames[0] != "approval" || variableNames[1] != "checkpoints" || variableNames[2] != "prompt" { - t.Fatalf("variableNames = %#v, want [approval checkpoints prompt]", variableNames) - } -} - -func TestParseABCVariableNamesRequiresAtLeastOneName(t *testing.T) { - t.Parallel() - - _, err := parseABCVariableNames(nil, nil) - if err == nil { - t.Fatal("parseABCVariableNames returned nil error, want usage error") - } - if _, ok := err.(*cmderrors.UsageError); !ok { - t.Fatalf("parseABCVariableNames error = %T, want *cmderrors.UsageError", err) - } -} - -func TestParseABCVariableNamesRejectsEmptyNames(t *testing.T) { - t.Parallel() - - _, err := parseABCVariableNames([]string{"approval", ""}, nil) - if err == nil { - t.Fatal("parseABCVariableNames returned nil error, want usage error") - } - if _, ok := err.(*cmderrors.UsageError); !ok { - t.Fatalf("parseABCVariableNames error = %T, want *cmderrors.UsageError", err) + want := []string{"approval", "checkpoints", "prompt"} + if diff := cmp.Diff(want, variableNames); diff != "" { + t.Fatalf("variableNames mismatch (-want +got):\n%s", diff) } } From daae1c26465b54adcf9050594ee41c5ed9a1e5f2 Mon Sep 17 00:00:00 2001 From: bahrmichael Date: Wed, 15 Apr 2026 12:20:57 +0200 Subject: [PATCH 11/16] move workflow instance id back --- cmd/src/abc.go | 24 ++------------- cmd/src/abc_variables.go | 26 +++------------- cmd/src/abc_variables_delete.go | 6 ++-- cmd/src/abc_variables_set.go | 10 +++---- cmd/src/cmd.go | 53 --------------------------------- 5 files changed, 15 insertions(+), 104 deletions(-) diff --git a/cmd/src/abc.go b/cmd/src/abc.go index 936d6df0d5..42d7fba85c 100644 --- a/cmd/src/abc.go +++ b/cmd/src/abc.go @@ -3,8 +3,6 @@ package main import ( "flag" "fmt" - - "github.com/sourcegraph/src-cli/internal/cmderrors" ) var abcCommands commander @@ -14,37 +12,21 @@ func init() { Usage: - src abc command [command options] + src abc command [command options] The commands are: variables manage workflow instance variables -Use "src abc [command] -h" for more information about a command. +Use "src abc [command] -h" for more information about a command. ` flagSet := flag.NewFlagSet("abc", flag.ExitOnError) usageFunc := func() { fmt.Println(usage) } - flagSet.Usage = usageFunc handler := func(args []string) error { - if err := flagSet.Parse(args); err != nil { - return err - } - - if flagSet.NArg() == 0 || flagSet.Arg(0) == "help" { - flagSet.SetOutput(flag.CommandLine.Output()) - flagSet.Usage() - return nil - } - - if flagSet.NArg() < 2 { - return cmderrors.Usage("must provide a workflow instance ID and subcommand") - } - - instanceID := flagSet.Arg(0) - abcCommands.runWithPrefixArgs("src abc ", []string{instanceID}, flagSet.Args()[1:]) + abcCommands.run(flagSet, "src abc", usage, args) return nil } diff --git a/cmd/src/abc_variables.go b/cmd/src/abc_variables.go index a21db8c21b..e1cf0857f0 100644 --- a/cmd/src/abc_variables.go +++ b/cmd/src/abc_variables.go @@ -3,49 +3,31 @@ package main import ( "flag" "fmt" - - "github.com/sourcegraph/src-cli/internal/cmderrors" ) var abcVariablesCommands commander func init() { - usage := `'src abc variables' is a tool that manages workflow instance variables. + usage := `'src abc variables' is a tool that manages workflow instance variables. Usage: - src abc variables command [command options] + src abc variables command [command options] The commands are: set set workflow instance variables delete delete workflow instance variables -Use "src abc variables [command] -h" for more information about a command. +Use "src abc variables [command] -h" for more information about a command. ` flagSet := flag.NewFlagSet("variables", flag.ExitOnError) usageFunc := func() { fmt.Println(usage) } - flagSet.Usage = usageFunc handler := func(args []string) error { - if len(args) == 0 { - return cmderrors.Usage("must provide a workflow instance ID") - } - - instanceID := args[0] - if err := flagSet.Parse(args[1:]); err != nil { - return err - } - - if flagSet.NArg() == 0 || flagSet.Arg(0) == "help" { - flagSet.SetOutput(flag.CommandLine.Output()) - flagSet.Usage() - return nil - } - - abcVariablesCommands.runWithPrefixArgs("src abc variables", []string{instanceID}, flagSet.Args()) + abcVariablesCommands.run(flagSet, "src abc variables", usage, args) return nil } diff --git a/cmd/src/abc_variables_delete.go b/cmd/src/abc_variables_delete.go index b4122cd5a0..af9fc78fe0 100644 --- a/cmd/src/abc_variables_delete.go +++ b/cmd/src/abc_variables_delete.go @@ -15,18 +15,18 @@ Examples: Delete a variable from a workflow instance: - $ src abc QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== variables delete approval + $ src abc variables delete QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== approval Delete multiple variables in one request: - $ src abc QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== variables delete --var approval --var checkpoints + $ src abc variables delete QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== --var approval --var checkpoints ` flagSet := flag.NewFlagSet("delete", flag.ExitOnError) var variableArgs abcVariableArgs flagSet.Var(&variableArgs, "var", "Variable name to delete. Repeat for multiple names.") usageFunc := func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src abc variables %s':\n", flagSet.Name()) + fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src abc variables %s':\n", flagSet.Name()) flagSet.PrintDefaults() fmt.Println(usage) } diff --git a/cmd/src/abc_variables_set.go b/cmd/src/abc_variables_set.go index be47a13034..54b524c651 100644 --- a/cmd/src/abc_variables_set.go +++ b/cmd/src/abc_variables_set.go @@ -27,15 +27,15 @@ Examples: Set a string variable on a workflow instance: - $ src abc QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== variables set prompt="tighten the review criteria" + $ src abc variables set QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== prompt="tighten the review criteria" Set multiple variables in one request: - $ src abc QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== variables set --var prompt="tighten the review criteria" --var checkpoints='[1,2,3]' + $ src abc variables set QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== --var prompt="tighten the review criteria" --var checkpoints='[1,2,3]' Set a structured JSON value: - $ src abc QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== variables set checkpoints='[1,2,3]' + $ src abc variables set QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== checkpoints='[1,2,3]' Values are interpreted as JSON literals when valid. Otherwise they are sent as plain strings. ` @@ -44,7 +44,7 @@ Values are interpreted as JSON literals when valid. Otherwise they are sent as p var variableArgs abcVariableArgs flagSet.Var(&variableArgs, "var", "Variable assignment in = form. Repeat to set multiple variables.") usageFunc := func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src abc variables %s':\n", flagSet.Name()) + fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src abc variables %s':\n", flagSet.Name()) flagSet.PrintDefaults() fmt.Println(usage) } @@ -145,7 +145,7 @@ func parseABCVariable(raw string) (abcVariable, error) { return abcVariable{}, err } if remove { - return abcVariable{}, cmderrors.Usagef("invalid variable assignment %q: use 'src abc variables delete %s' to remove a variable", raw, name) + return abcVariable{}, cmderrors.Usagef("invalid variable assignment %q: use 'src abc variables delete %s' to remove a variable", raw, name) } return abcVariable{Key: name, Value: value}, nil diff --git a/cmd/src/cmd.go b/cmd/src/cmd.go index e10454fd11..93d0accbe3 100644 --- a/cmd/src/cmd.go +++ b/cmd/src/cmd.go @@ -109,56 +109,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) } - -// runWithPrefixArgs runs a command tree where one or more leading arguments must -// be forwarded to the matched subcommand handler before its own arguments. -func (c commander) runWithPrefixArgs(cmdName string, prefixArgs, args []string) { - // Configure default usage funcs for commands. - for _, cmd := range c { - if cmd.usageFunc != nil { - cmd.flagSet.Usage = cmd.usageFunc - continue - } - cmd.flagSet.Usage = func() { - _, _ = fmt.Fprintf(flag.CommandLine.Output(), "Usage of '%s %s':\n", cmdName, cmd.flagSet.Name()) - cmd.flagSet.PrintDefaults() - } - } - - // Find the subcommand to execute. - name := args[0] - for _, cmd := range c { - if !cmd.matches(name) { - continue - } - - // Read global configuration now. - var err error - cfg, err = readConfig() - if err != nil { - log.Fatal("reading config: ", err) - } - - // Execute the subcommand. - if err := cmd.handler(append(slices.Clone(prefixArgs), 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()) - } - log.Fatal(err) - } - os.Exit(0) - } - - log.Printf("%s: unknown subcommand %q", cmdName, name) - log.Fatalf("Run '%s help' for usage.", cmdName) -} From a282361afe1509056a72cf18aab0efdf52d27f2c Mon Sep 17 00:00:00 2001 From: bahrmichael Date: Wed, 15 Apr 2026 12:46:32 +0200 Subject: [PATCH 12/16] test cleanup --- cmd/src/abc_variables_set_test.go | 56 ++----------------------------- 1 file changed, 3 insertions(+), 53 deletions(-) diff --git a/cmd/src/abc_variables_set_test.go b/cmd/src/abc_variables_set_test.go index 28ffff5658..fa92251dd5 100644 --- a/cmd/src/abc_variables_set_test.go +++ b/cmd/src/abc_variables_set_test.go @@ -23,26 +23,11 @@ func TestMarshalABCVariableValuePreservesLargeIntegerLiteral(t *testing.T) { } } -func TestMarshalABCVariableValueTreatsNullAsRemoval(t *testing.T) { - t.Parallel() - - value, remove, err := marshalABCVariableValue("null") - if err != nil { - t.Fatalf("marshalABCVariableValue returned error: %v", err) - } - if !remove { - t.Fatal("marshalABCVariableValue did not mark null for removal") - } - if value != "null" { - t.Fatalf("marshalABCVariableValue = %q, want %q", value, "null") - } -} - func TestParseABCVariables(t *testing.T) { t.Parallel() variables, err := parseABCVariables( - []string{"prompt=tighten the review criteria", `title="null"`}, + []string{"prompt=tighten the review criteria", `title="test"`}, abcVariableArgs{"checkpoints=[1,2,3]"}, ) if err != nil { @@ -57,8 +42,8 @@ func TestParseABCVariables(t *testing.T) { t.Fatalf("variables[0] = %#v, want prompt string variable", variables[0]) } - if variables[1].Key != "title" || variables[1].Value != `"null"` { - t.Fatalf("variables[1] = %#v, want quoted null string", variables[1]) + if variables[1].Key != "title" || variables[1].Value != `"test"` { + t.Fatalf("variables[1] = %#v, want quoted test string", variables[1]) } if variables[2].Key != "checkpoints" || variables[2].Value != "[1,2,3]" { @@ -66,38 +51,3 @@ func TestParseABCVariables(t *testing.T) { } } -func TestParseABCVariablesRequiresAssignments(t *testing.T) { - t.Parallel() - - _, err := parseABCVariables(nil, nil) - if err == nil { - t.Fatal("parseABCVariables returned nil error, want usage error") - } - if _, ok := err.(*cmderrors.UsageError); !ok { - t.Fatalf("parseABCVariables error = %T, want *cmderrors.UsageError", err) - } -} - -func TestParseABCVariableRequiresNameValueFormat(t *testing.T) { - t.Parallel() - - _, err := parseABCVariable("missing-separator") - if err == nil { - t.Fatal("parseABCVariable returned nil error, want usage error") - } - if _, ok := err.(*cmderrors.UsageError); !ok { - t.Fatalf("parseABCVariable error = %T, want *cmderrors.UsageError", err) - } -} - -func TestParseABCVariableRejectsNullLiteral(t *testing.T) { - t.Parallel() - - _, err := parseABCVariable("approval=null") - if err == nil { - t.Fatal("parseABCVariable returned nil error, want usage error") - } - if _, ok := err.(*cmderrors.UsageError); !ok { - t.Fatalf("parseABCVariable error = %T, want *cmderrors.UsageError", err) - } -} From 1a6476d3facd4ccee4f67c837b50fc3969c59918 Mon Sep 17 00:00:00 2001 From: bahrmichael Date: Wed, 15 Apr 2026 12:54:06 +0200 Subject: [PATCH 13/16] cleanup --- cmd/src/abc_variables.go | 2 +- cmd/src/abc_variables_set_test.go | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/cmd/src/abc_variables.go b/cmd/src/abc_variables.go index e1cf0857f0..1745a42dff 100644 --- a/cmd/src/abc_variables.go +++ b/cmd/src/abc_variables.go @@ -8,7 +8,7 @@ import ( var abcVariablesCommands commander func init() { - usage := `'src abc variables' is a tool that manages workflow instance variables. + usage := `'src abc variables' is a tool that manages workflow instance variables on agentic batch changes. Usage: diff --git a/cmd/src/abc_variables_set_test.go b/cmd/src/abc_variables_set_test.go index fa92251dd5..8acb23420b 100644 --- a/cmd/src/abc_variables_set_test.go +++ b/cmd/src/abc_variables_set_test.go @@ -2,8 +2,6 @@ package main import ( "testing" - - "github.com/sourcegraph/src-cli/internal/cmderrors" ) // If we were to do a json marshalling roundtrip, it may break large integer literals. @@ -50,4 +48,3 @@ func TestParseABCVariables(t *testing.T) { t.Fatalf("variables[2] = %#v, want compact JSON array", variables[2]) } } - From 38992fa7c497f39947af18b8f1828bf961d2623a Mon Sep 17 00:00:00 2001 From: bahrmichael Date: Wed, 15 Apr 2026 16:41:40 +0200 Subject: [PATCH 14/16] migrate to urfave/cli --- cmd/src/abc.go | 46 +++++++++------------ cmd/src/abc_variables.go | 48 +++++++++------------- cmd/src/abc_variables_delete.go | 66 +++++++++++++---------------- cmd/src/abc_variables_set.go | 73 +++++++++++++++------------------ cmd/src/run_migration_compat.go | 1 + 5 files changed, 100 insertions(+), 134 deletions(-) diff --git a/cmd/src/abc.go b/cmd/src/abc.go index 42d7fba85c..2380078ff7 100644 --- a/cmd/src/abc.go +++ b/cmd/src/abc.go @@ -1,38 +1,30 @@ package main import ( - "flag" - "fmt" -) + "context" -var abcCommands commander + "github.com/sourcegraph/src-cli/internal/clicompat" + "github.com/urfave/cli/v3" +) -func init() { - usage := `'src abc' is a tool that manages agentic batch changes. +var abcCommand = clicompat.WithLegacyHelp(&cli.Command{ + Name: "abc", + Usage: "manages agentic batch changes", + UsageText: `'src abc' is a tool that manages agentic batch changes. Usage: src abc command [command options] -The commands are: - - variables manage workflow instance variables - -Use "src abc [command] -h" for more information about a command. -` - - flagSet := flag.NewFlagSet("abc", flag.ExitOnError) - usageFunc := func() { - fmt.Println(usage) - } - handler := func(args []string) error { - abcCommands.run(flagSet, "src abc", usage, args) +The commands are:`, + Description: `Use "src abc [command] -h" for more information about a command.`, + HideHelpCommand: true, + HideVersion: true, + Commands: []*cli.Command{ + abcVariablesCommand, + }, + Action: func(ctx context.Context, c *cli.Command) error { + cli.HelpPrinter(c.Root().Writer, c.CustomRootCommandHelpTemplate, c) return nil - } - - commands = append(commands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usageFunc, - }) -} + }, +}) diff --git a/cmd/src/abc_variables.go b/cmd/src/abc_variables.go index 1745a42dff..b8c38c2678 100644 --- a/cmd/src/abc_variables.go +++ b/cmd/src/abc_variables.go @@ -1,39 +1,31 @@ package main import ( - "flag" - "fmt" -) + "context" -var abcVariablesCommands commander + "github.com/sourcegraph/src-cli/internal/clicompat" + "github.com/urfave/cli/v3" +) -func init() { - usage := `'src abc variables' is a tool that manages workflow instance variables on agentic batch changes. +var abcVariablesCommand = clicompat.WithLegacyHelp(&cli.Command{ + Name: "variables", + Usage: "manage workflow instance variables", + UsageText: `'src abc variables' is a tool that manages workflow instance variables on agentic batch changes. Usage: src abc variables command [command options] -The commands are: - - set set workflow instance variables - delete delete workflow instance variables - -Use "src abc variables [command] -h" for more information about a command. -` - - flagSet := flag.NewFlagSet("variables", flag.ExitOnError) - usageFunc := func() { - fmt.Println(usage) - } - handler := func(args []string) error { - abcVariablesCommands.run(flagSet, "src abc variables", usage, args) +The commands are:`, + Description: `Use "src abc variables [command] -h" for more information about a command.`, + HideHelpCommand: true, + HideVersion: true, + Commands: []*cli.Command{ + abcVariablesSetCommand, + abcVariablesDeleteCommand, + }, + Action: func(ctx context.Context, c *cli.Command) error { + cli.HelpPrinter(c.Root().Writer, c.CustomRootCommandHelpTemplate, c) return nil - } - - abcCommands = append(abcCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usageFunc, - }) -} + }, +}) diff --git a/cmd/src/abc_variables_delete.go b/cmd/src/abc_variables_delete.go index af9fc78fe0..d888252955 100644 --- a/cmd/src/abc_variables_delete.go +++ b/cmd/src/abc_variables_delete.go @@ -2,48 +2,43 @@ package main import ( "context" - "flag" "fmt" - "github.com/sourcegraph/src-cli/internal/api" + "github.com/sourcegraph/src-cli/internal/clicompat" "github.com/sourcegraph/src-cli/internal/cmderrors" + "github.com/urfave/cli/v3" ) -func init() { - usage := ` +var abcVariablesDeleteCommand = clicompat.Wrap(&cli.Command{ + Name: "delete", + Usage: "delete workflow instance variables", + Description: `Usage: + + src abc variables delete [command options] [ ...] + Examples: Delete a variable from a workflow instance: - $ src abc variables delete QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== approval + $ src abc variables delete QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== approval Delete multiple variables in one request: - $ src abc variables delete QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== --var approval --var checkpoints - ` - - flagSet := flag.NewFlagSet("delete", flag.ExitOnError) - var variableArgs abcVariableArgs - flagSet.Var(&variableArgs, "var", "Variable name to delete. Repeat for multiple names.") - usageFunc := func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src abc variables %s':\n", flagSet.Name()) - flagSet.PrintDefaults() - fmt.Println(usage) - } - apiFlags := api.NewFlags(flagSet) - - handler := func(args []string) error { - if len(args) == 0 { + $ src abc variables delete QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== --var approval --var checkpoints`, + DisableSliceFlagSeparator: true, + Flags: clicompat.WithAPIFlags( + &cli.StringSliceFlag{ + Name: "var", + Usage: "Variable name to delete. Repeat for multiple names.", + }, + ), + Action: func(ctx context.Context, c *cli.Command) error { + if c.NArg() == 0 { return cmderrors.Usage("must provide a workflow instance ID") } - instanceID := args[0] - variableArgs = nil - if err := flagSet.Parse(args[1:]); err != nil { - return err - } - - variableNames, err := parseABCVariableNames(flagSet.Args(), variableArgs) + instanceID := c.Args().First() + variableNames, err := parseABCVariableNames(c.Args().Tail(), abcVariableArgs(c.StringSlice("var"))) if err != nil { return err } @@ -56,8 +51,9 @@ Examples: }) } - client := cfg.apiClient(apiFlags, flagSet.Output()) - if err := updateABCWorkflowInstanceVariables(context.Background(), client, instanceID, variables); err != nil { + apiFlags := clicompat.APIFlagsFromCmd(c) + client := cfg.apiClient(apiFlags, c.Writer) + if err := updateABCWorkflowInstanceVariables(ctx, client, instanceID, variables); err != nil { return err } @@ -65,16 +61,10 @@ Examples: return nil } - fmt.Printf("Removed variables %q from workflow instance %q.\n", variableNames, instanceID) + fmt.Fprintf(c.Writer, "Removed variables %q from workflow instance %q.\n", variableNames, instanceID) return nil - } - - abcVariablesCommands = append(abcVariablesCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usageFunc, - }) -} + }, +}) func parseABCVariableNames(positional []string, flagged abcVariableArgs) ([]string, error) { variableNames := append([]string{}, positional...) diff --git a/cmd/src/abc_variables_set.go b/cmd/src/abc_variables_set.go index 54b524c651..6b97cdb9f4 100644 --- a/cmd/src/abc_variables_set.go +++ b/cmd/src/abc_variables_set.go @@ -4,12 +4,13 @@ import ( "bytes" "context" "encoding/json" - "flag" "fmt" "strings" "github.com/sourcegraph/src-cli/internal/api" + "github.com/sourcegraph/src-cli/internal/clicompat" "github.com/sourcegraph/src-cli/internal/cmderrors" + "github.com/urfave/cli/v3" ) const updateABCWorkflowInstanceVariablesMutation = `mutation UpdateAgenticWorkflowInstanceVariables( @@ -21,47 +22,42 @@ const updateABCWorkflowInstanceVariablesMutation = `mutation UpdateAgenticWorkfl } }` -func init() { - usage := ` +var abcVariablesSetCommand = clicompat.Wrap(&cli.Command{ + Name: "set", + Usage: "set workflow instance variables", + Description: `Usage: + + src abc variables set [command options] [= ...] + Examples: Set a string variable on a workflow instance: - $ src abc variables set QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== prompt="tighten the review criteria" + $ src abc variables set QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== prompt="tighten the review criteria" Set multiple variables in one request: - $ src abc variables set QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== --var prompt="tighten the review criteria" --var checkpoints='[1,2,3]' + $ src abc variables set QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== --var prompt="tighten the review criteria" --var checkpoints='[1,2,3]' Set a structured JSON value: - $ src abc variables set QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== checkpoints='[1,2,3]' - -Values are interpreted as JSON literals when valid. Otherwise they are sent as plain strings. - ` - - flagSet := flag.NewFlagSet("set", flag.ExitOnError) - var variableArgs abcVariableArgs - flagSet.Var(&variableArgs, "var", "Variable assignment in = form. Repeat to set multiple variables.") - usageFunc := func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src abc variables %s':\n", flagSet.Name()) - flagSet.PrintDefaults() - fmt.Println(usage) - } - apiFlags := api.NewFlags(flagSet) - - handler := func(args []string) error { - if len(args) == 0 { + $ src abc variables set QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== checkpoints='[1,2,3]' + +Values are interpreted as JSON literals when valid. Otherwise they are sent as plain strings.`, + DisableSliceFlagSeparator: true, + Flags: clicompat.WithAPIFlags( + &cli.StringSliceFlag{ + Name: "var", + Usage: "Variable assignment in = form. Repeat to set multiple variables.", + }, + ), + Action: func(ctx context.Context, c *cli.Command) error { + if c.NArg() == 0 { return cmderrors.Usage("must provide a workflow instance ID") } - instanceID := args[0] - variableArgs = nil - if err := flagSet.Parse(args[1:]); err != nil { - return err - } - - variables, err := parseABCVariables(flagSet.Args(), variableArgs) + instanceID := c.Args().First() + variables, err := parseABCVariables(c.Args().Tail(), abcVariableArgs(c.StringSlice("var"))) if err != nil { return err } @@ -74,8 +70,9 @@ Values are interpreted as JSON literals when valid. Otherwise they are sent as p }) } - client := cfg.apiClient(apiFlags, flagSet.Output()) - if err := updateABCWorkflowInstanceVariables(context.Background(), client, instanceID, graphqlVariables); err != nil { + apiFlags := clicompat.APIFlagsFromCmd(c) + client := cfg.apiClient(apiFlags, c.Writer) + if err := updateABCWorkflowInstanceVariables(ctx, client, instanceID, graphqlVariables); err != nil { return err } @@ -84,20 +81,14 @@ Values are interpreted as JSON literals when valid. Otherwise they are sent as p } if len(variables) == 1 { - fmt.Printf("Set variable %q on workflow instance %q.\n", variables[0].Key, instanceID) + fmt.Fprintf(c.Writer, "Set variable %q on workflow instance %q.\n", variables[0].Key, instanceID) return nil } - fmt.Printf("Updated %d variables on workflow instance %q.\n", len(variables), instanceID) + fmt.Fprintf(c.Writer, "Updated %d variables on workflow instance %q.\n", len(variables), instanceID) return nil - } - - abcVariablesCommands = append(abcVariablesCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usageFunc, - }) -} + }, +}) type abcVariableArgs []string diff --git a/cmd/src/run_migration_compat.go b/cmd/src/run_migration_compat.go index c50f2cd34a..6c2f72e209 100644 --- a/cmd/src/run_migration_compat.go +++ b/cmd/src/run_migration_compat.go @@ -16,6 +16,7 @@ import ( ) var migratedCommands = map[string]*cli.Command{ + "abc": abcCommand, "version": versionCommand, } From e2bd681be9a1494b7b7aa40673e3bdfd2ec6d264 Mon Sep 17 00:00:00 2001 From: bahrmichael Date: Wed, 15 Apr 2026 16:47:56 +0200 Subject: [PATCH 15/16] cleanup --- cmd/src/abc.go | 10 +++++----- cmd/src/abc_variables.go | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cmd/src/abc.go b/cmd/src/abc.go index 2380078ff7..80bf4d1c51 100644 --- a/cmd/src/abc.go +++ b/cmd/src/abc.go @@ -7,7 +7,7 @@ import ( "github.com/urfave/cli/v3" ) -var abcCommand = clicompat.WithLegacyHelp(&cli.Command{ +var abcCommand = &cli.Command{ Name: "abc", Usage: "manages agentic batch changes", UsageText: `'src abc' is a tool that manages agentic batch changes. @@ -17,14 +17,14 @@ Usage: src abc command [command options] The commands are:`, + OnUsageError: clicompat.OnUsageError, Description: `Use "src abc [command] -h" for more information about a command.`, HideHelpCommand: true, HideVersion: true, Commands: []*cli.Command{ abcVariablesCommand, }, - Action: func(ctx context.Context, c *cli.Command) error { - cli.HelpPrinter(c.Root().Writer, c.CustomRootCommandHelpTemplate, c) - return nil + Action: func(_ context.Context, c *cli.Command) error { + return cli.ShowSubcommandHelp(c) }, -}) +} diff --git a/cmd/src/abc_variables.go b/cmd/src/abc_variables.go index b8c38c2678..d1f9a61f12 100644 --- a/cmd/src/abc_variables.go +++ b/cmd/src/abc_variables.go @@ -7,7 +7,7 @@ import ( "github.com/urfave/cli/v3" ) -var abcVariablesCommand = clicompat.WithLegacyHelp(&cli.Command{ +var abcVariablesCommand = &cli.Command{ Name: "variables", Usage: "manage workflow instance variables", UsageText: `'src abc variables' is a tool that manages workflow instance variables on agentic batch changes. @@ -17,6 +17,7 @@ Usage: src abc variables command [command options] The commands are:`, + OnUsageError: clicompat.OnUsageError, Description: `Use "src abc variables [command] -h" for more information about a command.`, HideHelpCommand: true, HideVersion: true, @@ -24,8 +25,7 @@ The commands are:`, abcVariablesSetCommand, abcVariablesDeleteCommand, }, - Action: func(ctx context.Context, c *cli.Command) error { - cli.HelpPrinter(c.Root().Writer, c.CustomRootCommandHelpTemplate, c) - return nil + Action: func(_ context.Context, c *cli.Command) error { + return cli.ShowSubcommandHelp(c) }, -}) +} From e63b5f4205100d724fc8d0f07937b06c777ca84e Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Thu, 16 Apr 2026 11:57:10 +0200 Subject: [PATCH 16/16] migrate abc command to use urfave/cli (#1297) --- cmd/src/abc.go | 31 ++++++------ cmd/src/abc_variables.go | 31 ------------ cmd/src/abc_variables_delete.go | 88 +++++++++++++++++--------------- cmd/src/abc_variables_set.go | 90 +++++++++++++++++---------------- 4 files changed, 109 insertions(+), 131 deletions(-) delete mode 100644 cmd/src/abc_variables.go diff --git a/cmd/src/abc.go b/cmd/src/abc.go index 80bf4d1c51..5d2b019556 100644 --- a/cmd/src/abc.go +++ b/cmd/src/abc.go @@ -7,24 +7,23 @@ import ( "github.com/urfave/cli/v3" ) -var abcCommand = &cli.Command{ +var abcCommand = clicompat.Wrap(&cli.Command{ Name: "abc", Usage: "manages agentic batch changes", - UsageText: `'src abc' is a tool that manages agentic batch changes. - -Usage: - - src abc command [command options] - -The commands are:`, - OnUsageError: clicompat.OnUsageError, - Description: `Use "src abc [command] -h" for more information about a command.`, - HideHelpCommand: true, - HideVersion: true, Commands: []*cli.Command{ - abcVariablesCommand, + clicompat.Wrap(&cli.Command{ + Name: "variables", + Usage: "manage workflow instance variables", + Commands: []*cli.Command{ + abcVariablesSetCommand, + abcVariablesDeleteCommand, + }, + Action: func(ctx context.Context, cmd *cli.Command) error { + return cli.ShowSubcommandHelp(cmd) + }, + }), }, - Action: func(_ context.Context, c *cli.Command) error { - return cli.ShowSubcommandHelp(c) + Action: func(ctx context.Context, cmd *cli.Command) error { + return cli.ShowSubcommandHelp(cmd) }, -} +}) diff --git a/cmd/src/abc_variables.go b/cmd/src/abc_variables.go deleted file mode 100644 index d1f9a61f12..0000000000 --- a/cmd/src/abc_variables.go +++ /dev/null @@ -1,31 +0,0 @@ -package main - -import ( - "context" - - "github.com/sourcegraph/src-cli/internal/clicompat" - "github.com/urfave/cli/v3" -) - -var abcVariablesCommand = &cli.Command{ - Name: "variables", - Usage: "manage workflow instance variables", - UsageText: `'src abc variables' is a tool that manages workflow instance variables on agentic batch changes. - -Usage: - - src abc variables command [command options] - -The commands are:`, - OnUsageError: clicompat.OnUsageError, - Description: `Use "src abc variables [command] -h" for more information about a command.`, - HideHelpCommand: true, - HideVersion: true, - Commands: []*cli.Command{ - abcVariablesSetCommand, - abcVariablesDeleteCommand, - }, - Action: func(_ context.Context, c *cli.Command) error { - return cli.ShowSubcommandHelp(c) - }, -} diff --git a/cmd/src/abc_variables_delete.go b/cmd/src/abc_variables_delete.go index d888252955..76d1dd4567 100644 --- a/cmd/src/abc_variables_delete.go +++ b/cmd/src/abc_variables_delete.go @@ -3,81 +3,87 @@ package main import ( "context" "fmt" + "io" + "slices" + "github.com/sourcegraph/src-cli/internal/api" "github.com/sourcegraph/src-cli/internal/clicompat" "github.com/sourcegraph/src-cli/internal/cmderrors" "github.com/urfave/cli/v3" ) var abcVariablesDeleteCommand = clicompat.Wrap(&cli.Command{ - Name: "delete", - Usage: "delete workflow instance variables", - Description: `Usage: - - src abc variables delete [command options] [ ...] + Name: "delete", + Usage: "Delete variables on a workflow instance", + UsageText: "src abc variables delete [options] [ ...]", + Description: ` +Delete workflow instance variables Examples: Delete a variable from a workflow instance: - $ src abc variables delete QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== approval + $ src abc variables delete QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== approval Delete multiple variables in one request: - $ src abc variables delete QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== --var approval --var checkpoints`, - DisableSliceFlagSeparator: true, + $ src abc variables delete QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== --var approval --var checkpoints +`, Flags: clicompat.WithAPIFlags( &cli.StringSliceFlag{ Name: "var", Usage: "Variable name to delete. Repeat for multiple names.", }, ), - Action: func(ctx context.Context, c *cli.Command) error { - if c.NArg() == 0 { + Action: func(ctx context.Context, cmd *cli.Command) error { + if !cmd.Args().Present() { return cmderrors.Usage("must provide a workflow instance ID") } - instanceID := c.Args().First() - variableNames, err := parseABCVariableNames(c.Args().Tail(), abcVariableArgs(c.StringSlice("var"))) - if err != nil { - return err - } - - variables := make([]map[string]string, 0, len(variableNames)) - for _, key := range variableNames { - variables = append(variables, map[string]string{ - "key": key, - "value": "null", - }) - } - - apiFlags := clicompat.APIFlagsFromCmd(c) - client := cfg.apiClient(apiFlags, c.Writer) - if err := updateABCWorkflowInstanceVariables(ctx, client, instanceID, variables); err != nil { - return err - } + instanceID := cmd.Args().First() + client := cfg.apiClient(clicompat.APIFlagsFromCmd(cmd), cmd.Writer) + varArgs := abcVariableArgs(cmd.StringSlice("var")) - if apiFlags.GetCurl() { - return nil + if len(varArgs) == 0 { + return cmderrors.Usage("must provide at least one variable name") } - - fmt.Fprintf(c.Writer, "Removed variables %q from workflow instance %q.\n", variableNames, instanceID) - return nil + return runABCVariablesDelete(ctx, client, instanceID, cmd.Args().Tail(), varArgs, cmd.Writer, cmd.Bool("get-curl")) }, }) func parseABCVariableNames(positional []string, flagged abcVariableArgs) ([]string, error) { variableNames := append([]string{}, positional...) variableNames = append(variableNames, flagged...) - if len(variableNames) == 0 { - return nil, cmderrors.Usage("must provide at least one variable name") - } - for _, name := range variableNames { - if name == "" { - return nil, cmderrors.Usage("variable names must not be empty") - } + if slices.Contains(variableNames, "") { + return nil, cmderrors.Usage("variable names must not be empty") } return variableNames, nil } + +func runABCVariablesDelete(ctx context.Context, client api.Client, instanceID string, positional []string, flagged abcVariableArgs, output io.Writer, getCurl bool) error { + variableNames, err := parseABCVariableNames(positional, flagged) + if err != nil { + return err + } + + variables := make([]map[string]string, 0, len(variableNames)) + for _, key := range variableNames { + variables = append(variables, map[string]string{ + "key": key, + "value": "null", + }) + } + + if err := updateABCWorkflowInstanceVariables(ctx, client, instanceID, variables); err != nil { + return err + } + + if getCurl { + return nil + } + + _, err = fmt.Fprintf(output, "Removed variables %q from workflow instance %q.\n", variableNames, instanceID) + return err +} diff --git a/cmd/src/abc_variables_set.go b/cmd/src/abc_variables_set.go index 6b97cdb9f4..58f5c3b8be 100644 --- a/cmd/src/abc_variables_set.go +++ b/cmd/src/abc_variables_set.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "io" "strings" "github.com/sourcegraph/src-cli/internal/api" @@ -23,70 +24,42 @@ const updateABCWorkflowInstanceVariablesMutation = `mutation UpdateAgenticWorkfl }` var abcVariablesSetCommand = clicompat.Wrap(&cli.Command{ - Name: "set", - Usage: "set workflow instance variables", - Description: `Usage: - - src abc variables set [command options] [= ...] + Name: "set", + UsageText: "src abc variables set [options] [= ...]", + Usage: "Set variables on a workflow instance", + Description: ` +Set workflow instance variables Examples: Set a string variable on a workflow instance: - $ src abc variables set QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== prompt="tighten the review criteria" + $ src abc variables set QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== prompt="tighten the review criteria" Set multiple variables in one request: - $ src abc variables set QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== --var prompt="tighten the review criteria" --var checkpoints='[1,2,3]' + $ src abc variables set QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== --var prompt="tighten the review criteria" --var checkpoints='[1,2,3]' Set a structured JSON value: - $ src abc variables set QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== checkpoints='[1,2,3]' + $ src abc variables set QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== checkpoints='[1,2,3]' -Values are interpreted as JSON literals when valid. Otherwise they are sent as plain strings.`, - DisableSliceFlagSeparator: true, +NOTE: Values are interpreted as JSON literals when valid. Otherwise they are sent as plain strings. +`, Flags: clicompat.WithAPIFlags( &cli.StringSliceFlag{ Name: "var", Usage: "Variable assignment in = form. Repeat to set multiple variables.", }, ), - Action: func(ctx context.Context, c *cli.Command) error { - if c.NArg() == 0 { + Action: func(ctx context.Context, cmd *cli.Command) error { + if !cmd.Args().Present() { return cmderrors.Usage("must provide a workflow instance ID") } - instanceID := c.Args().First() - variables, err := parseABCVariables(c.Args().Tail(), abcVariableArgs(c.StringSlice("var"))) - if err != nil { - return err - } - - graphqlVariables := make([]map[string]string, 0, len(variables)) - for _, variable := range variables { - graphqlVariables = append(graphqlVariables, map[string]string{ - "key": variable.Key, - "value": variable.Value, - }) - } - - apiFlags := clicompat.APIFlagsFromCmd(c) - client := cfg.apiClient(apiFlags, c.Writer) - if err := updateABCWorkflowInstanceVariables(ctx, client, instanceID, graphqlVariables); err != nil { - return err - } - - if apiFlags.GetCurl() { - return nil - } - - if len(variables) == 1 { - fmt.Fprintf(c.Writer, "Set variable %q on workflow instance %q.\n", variables[0].Key, instanceID) - return nil - } - - fmt.Fprintf(c.Writer, "Updated %d variables on workflow instance %q.\n", len(variables), instanceID) - return nil + instanceID := cmd.Args().First() + client := cfg.apiClient(clicompat.APIFlagsFromCmd(cmd), cmd.Writer) + return runABCVariablesSet(ctx, client, instanceID, cmd.Args().Tail(), abcVariableArgs(cmd.StringSlice("var")), cmd.Writer, cmd.Bool("get-curl")) }, }) @@ -142,6 +115,37 @@ func parseABCVariable(raw string) (abcVariable, error) { return abcVariable{Key: name, Value: value}, nil } +func runABCVariablesSet(ctx context.Context, client api.Client, instanceID string, positional []string, flagged abcVariableArgs, output io.Writer, getCurl bool) error { + variables, err := parseABCVariables(positional, flagged) + if err != nil { + return err + } + + graphqlVariables := make([]map[string]string, 0, len(variables)) + for _, variable := range variables { + graphqlVariables = append(graphqlVariables, map[string]string{ + "key": variable.Key, + "value": variable.Value, + }) + } + + if err := updateABCWorkflowInstanceVariables(ctx, client, instanceID, graphqlVariables); err != nil { + return err + } + + if getCurl { + return nil + } + + if len(variables) == 1 { + _, err = fmt.Fprintf(output, "Set variable %q on workflow instance %q.\n", variables[0].Key, instanceID) + return err + } + + _, err = fmt.Fprintf(output, "Updated %d variables on workflow instance %q.\n", len(variables), instanceID) + return err +} + func updateABCWorkflowInstanceVariables(ctx context.Context, client api.Client, instanceID string, variables []map[string]string) error { var result struct { UpdateAgenticWorkflowInstanceVariables struct {