Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions cmd/src/abc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package main

import (
"context"

"github.com/sourcegraph/src-cli/internal/clicompat"
"github.com/urfave/cli/v3"
)

var abcCommand = clicompat.Wrap(&cli.Command{
Name: "abc",
Usage: "manages agentic batch changes",
Commands: []*cli.Command{
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(ctx context.Context, cmd *cli.Command) error {
return cli.ShowSubcommandHelp(cmd)
},
})
76 changes: 76 additions & 0 deletions cmd/src/abc_variables_delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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 variables on a workflow instance",
UsageText: "src abc variables delete [options] <workflow-instance-id> [<name> ...]",
DisableSliceFlagSeparator: true,
Description: `
Delete workflow instance variables

Examples:

Delete a variable from a workflow instance:

$ src abc variables delete QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== approval

Delete multiple variables in one request:

$ 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, cmd *cli.Command) error {
if !cmd.Args().Present() {
return cmderrors.Usage("must provide a workflow instance ID")
}

instanceID := cmd.Args().First()
client := cfg.apiClient(clicompat.APIFlagsFromCmd(cmd), cmd.Writer)
variableNames := append(cmd.Args().Tail(), cmd.StringSlice("var")...)

return runABCVariablesDelete(ctx, client, instanceID, variableNames, cmd.Writer)
},
})

func runABCVariablesDelete(ctx context.Context, client api.Client, instanceID string, variableNames []string, output io.Writer) error {
if len(variableNames) == 0 {
return cmderrors.Usage("must provide at least one variable name")
}

if slices.Contains(variableNames, "") {
return cmderrors.Usage("variable names must not be empty")
}

variables := make([]map[string]string, 0, len(variableNames))
for _, key := range variableNames {
variables = append(variables, map[string]string{
"key": key,
"value": "null",
})
}

ok, err := updateABCWorkflowInstanceVariables(ctx, client, instanceID, variables)
if err != nil || !ok {
return err
}

_, err = fmt.Fprintf(output, "Removed variables %q from workflow instance %q.\n", variableNames, instanceID)
return err
}
71 changes: 71 additions & 0 deletions cmd/src/abc_variables_delete_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package main

import (
"bytes"
"context"
"io"
"testing"

mockapi "github.com/sourcegraph/src-cli/internal/api/mock"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

func TestRunABCVariablesDelete(t *testing.T) {
t.Parallel()

client := new(mockapi.Client)
request := &mockapi.Request{Response: `{"data":{"updateAgenticWorkflowInstanceVariables":{"id":"workflow"}}}`}
output := &bytes.Buffer{}
variableNames := []string{"approval", "checkpoints", "prompt"}

client.On("NewRequest", updateABCWorkflowInstanceVariablesMutation, map[string]any{
"instanceID": "QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ==",
"variables": []map[string]string{
{"key": "approval", "value": "null"},
{"key": "checkpoints", "value": "null"},
{"key": "prompt", "value": "null"},
},
}).Return(request).Once()
request.On("Do", context.Background(), mock.Anything).Return(true, nil).Once()

err := runABCVariablesDelete(context.Background(), client, "QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ==", variableNames, output)
require.NoError(t, err)
require.Equal(t, "Removed variables [\"approval\" \"checkpoints\" \"prompt\"] from workflow instance \"QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ==\".\n", output.String())

client.AssertExpectations(t)
request.AssertExpectations(t)
}

func TestRunABCVariablesDeleteRejectsEmptyVariableName(t *testing.T) {
t.Parallel()

err := runABCVariablesDelete(context.Background(), nil, "QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ==", []string{"approval", ""}, io.Discard)
require.ErrorContains(t, err, "variable names must not be empty")
}

func TestRunABCVariablesDeleteSuppressesSuccessMessageWhenRequestDoesNotExecute(t *testing.T) {
t.Parallel()

client := new(mockapi.Client)
request := &mockapi.Request{}
output := &bytes.Buffer{}
variableNames := []string{"approval", "checkpoints", "prompt"}

client.On("NewRequest", updateABCWorkflowInstanceVariablesMutation, map[string]any{
"instanceID": "QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ==",
"variables": []map[string]string{
{"key": "approval", "value": "null"},
{"key": "checkpoints", "value": "null"},
{"key": "prompt", "value": "null"},
},
}).Return(request).Once()
request.On("Do", context.Background(), mock.Anything).Return(false, nil).Once()

err := runABCVariablesDelete(context.Background(), client, "QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ==", variableNames, output)
require.NoError(t, err)
require.Empty(t, output.String())

client.AssertExpectations(t)
request.AssertExpectations(t)
}
155 changes: 155 additions & 0 deletions cmd/src/abc_variables_set.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package main

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"sort"
"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(
$instanceID: ID!,
$variables: [AgenticWorkflowInstanceVariableInput!]!,
) {
updateAgenticWorkflowInstanceVariables(instanceID: $instanceID, variables: $variables) {
id
}
}`

var abcVariablesSetCommand = clicompat.Wrap(&cli.Command{
Name: "set",
UsageText: "src abc variables set [options] <workflow-instance-id> [<name>=<value> ...]",
Usage: "Set variables on a workflow instance",
DisableSliceFlagSeparator: true,
Description: `
Set workflow instance variables

Examples:

Set a string variable on a workflow instance:

$ 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]'

Set a structured JSON value:

$ src abc variables set QWdlbnRpY1dvcmtmbG93SW5zdGFuY2U6MQ== checkpoints='[1,2,3]'

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 <name>=<value> form. Repeat to set multiple variables.",
},
),
Action: func(ctx context.Context, cmd *cli.Command) error {
if !cmd.Args().Present() {
return cmderrors.Usage("must provide a workflow instance ID")
}

instanceID := cmd.Args().First()
client := cfg.apiClient(clicompat.APIFlagsFromCmd(cmd), cmd.Writer)
abcVariables, err := parseABCVariables(cmd.Args().Tail(), cmd.StringSlice("var"))
if err != nil {
return err
}
return runABCVariablesSet(ctx, client, instanceID, abcVariables, cmd.Writer)
},
})

func parseABCVariables(positional []string, flagged []string) (map[string]string, error) {
rawVariables := append(positional, flagged...)
if len(rawVariables) == 0 {
return nil, cmderrors.Usage("must provide at least one variable assignment")
}

variables := make(map[string]string, len(rawVariables))
for _, v := range rawVariables {
name, rawValue, ok := strings.Cut(v, "=")
if !ok || name == "" {
return nil, cmderrors.Usagef("invalid variable assignment %q: must be in <name>=<value> form", v)
}

value, remove, err := marshalABCVariableValue(rawValue)
if err != nil {
return nil, err
}
if remove {
return nil, cmderrors.Usagef("invalid variable assignment %q: use 'src abc variables delete <workflow-instance-id> %s' to remove a variable", rawValue, name)
}

variables[name] = value
}

return variables, nil
}

func runABCVariablesSet(ctx context.Context, client api.Client, instanceID string, variables map[string]string, output io.Writer) error {
graphqlVariables := make([]map[string]string, 0, len(variables))
keys := make([]string, 0, len(variables))
for k := range variables {
keys = append(keys, k)
}
sort.Strings(keys)

for _, k := range keys {
graphqlVariables = append(graphqlVariables, map[string]string{
"key": k,
"value": variables[k],
})
}

ok, err := updateABCWorkflowInstanceVariables(ctx, client, instanceID, graphqlVariables)
if err != nil || !ok {
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) (bool, 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 ok, err
}

return true, nil
}

func marshalABCVariableValue(raw string) (value string, remove bool, err error) {
// 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()
return value, value == "null", nil
}

encoded, err := json.Marshal(raw)
if err != nil {
return "", false, err
}

return string(encoded), false, nil
}
Loading
Loading