Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 9 additions & 2 deletions cmd/sync/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ import (
syncprompt "github.com/launchdarkly/ldcli/internal/sync/prompt"
)

const dryRunFlag = "dry-run"
const (
addFlag = "add"
dryRunFlag = "dry-run"
)

func NewPromptCmd(client resources.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "prompt",
Short: "Synchronize local prompt variations with LaunchDarkly",
Long: "Plan synchronization changes for local prompt variations. Use --dry-run to preview changes without creating a plan.",
Long: "Bootstrap local prompt variations from LaunchDarkly, add more variations, or preview synchronization changes.",
Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.NoArgs(cmd, args); err != nil {
return err
Expand All @@ -30,6 +33,7 @@ func NewPromptCmd(client resources.Client) *cobra.Command {
},
RunE: runPrompt(client),
}
cmd.Flags().Bool(addFlag, false, "Select additional prompt variations from LaunchDarkly")
cmd.Flags().Bool(dryRunFlag, false, "Preview synchronization changes without creating a plan")
cmd.SetUsageTemplate(resourcescmd.SubcommandUsageTemplate())
return cmd
Expand All @@ -41,14 +45,17 @@ func runPrompt(client resources.Client) func(*cobra.Command, []string) error {
if err != nil {
return fmt.Errorf("get working directory: %w", err)
}
add, _ := cmd.Flags().GetBool(addFlag)
dryRun, _ := cmd.Flags().GetBool(dryRunFlag)
outputKind := cliflags.GetOutputKind(cmd)
err = syncprompt.NewRunner(client).Run(syncprompt.Options{
WorkingDirectory: workingDirectory,
AccessToken: viper.GetString(cliflags.AccessTokenFlag),
BaseURI: viper.GetString(cliflags.BaseURIFlag),
OutputKind: outputKind,
Add: add,
DryRun: dryRun,
Input: cmd.InOrStdin(),
Output: cmd.OutOrStdout(),
})
if err != nil {
Expand Down
193 changes: 193 additions & 0 deletions internal/sync/api/catalog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package api

import (
"encoding/json"
"fmt"
"maps"
"net/http"
"net/url"

"github.com/launchdarkly/ldcli/internal/resources"
syncdomain "github.com/launchdarkly/ldcli/internal/sync"
)

const catalogPageLimit = 25

type Project struct {
Key string `json:"key"`
Name string `json:"name"`
}

type Config struct {
Key string `json:"key"`
Name string `json:"name"`
Mode syncdomain.VariationMode `json:"mode"`
Variations []syncdomain.Variation `json:"variations"`
}

type CatalogClient struct {
transport resources.Client
accessToken string
baseURI string
}

func NewCatalogClient(
transport resources.Client,
accessToken string,
baseURI string,
) CatalogClient {
return CatalogClient{
transport: transport,
accessToken: accessToken,
baseURI: baseURI,
}
}

func (client CatalogClient) Projects() ([]Project, error) {
endpoint, err := url.JoinPath(client.baseURI, "api/v2/projects")
if err != nil {
return nil, fmt.Errorf("build projects endpoint: %w", err)
}

return listCatalog[Project](
client,
endpoint,
"projects",
false,
url.Values{"sort": {"name"}},
)
}

func (client CatalogClient) Configs(projectKey string) ([]Config, error) {
endpoint, err := url.JoinPath(
client.baseURI,
"api/v2/projects",
projectKey,
"ai-configs",
)
if err != nil {
return nil, fmt.Errorf("build AI Configs endpoint: %w", err)
}

configs, err := listCatalog[Config](
client,
endpoint,
"AI Configs",
false,
url.Values{
"sort": {"name"},
"filter": {`mode anyOf ["agent","completion"]`},
},
)
if err != nil {
return nil, err
}

for index := range configs {
if err := configs[index].applyMode(); err != nil {
return nil, err
}
}

return configs, nil
}

func (client CatalogClient) Config(projectKey, configKey string) (Config, error) {
endpoint, err := url.JoinPath(
client.baseURI,
"api/v2/projects",
projectKey,
"ai-configs",
configKey,
)
if err != nil {
return Config{}, fmt.Errorf("build AI Config endpoint: %w", err)
}

response, err := client.transport.MakeRequest(
client.accessToken,
http.MethodGet,
endpoint,
"",
nil,
nil,
false,
)
if err != nil {
return Config{}, fmt.Errorf("get AI Config %q: %w", configKey, err)
}

var config Config
if err := json.Unmarshal(response, &config); err != nil {
return Config{}, fmt.Errorf("decode AI Config response: %w", err)
}
if err := config.applyMode(); err != nil {
return Config{}, err
}

return config, nil
}

func (config *Config) applyMode() error {
if config.Mode == "" {
config.Mode = syncdomain.VariationModeCompletion
}
if !config.Mode.Valid() {
return fmt.Errorf(
"AI Config %q has unsupported mode %q",
config.Key,
config.Mode,
)
}

for index := range config.Variations {
config.Variations[index].Mode = config.Mode
}

return nil
}

type catalogPage[T any] struct {
Items []T `json:"items"`
TotalCount int `json:"totalCount"`
}

func listCatalog[T any](
client CatalogClient,
endpoint string,
resourceName string,
beta bool,
baseQuery url.Values,
) ([]T, error) {
var items []T

for offset := 0; ; offset += catalogPageLimit {
query := maps.Clone(baseQuery)
query.Set("limit", fmt.Sprintf("%d", catalogPageLimit))
query.Set("offset", fmt.Sprintf("%d", offset))

response, err := client.transport.MakeRequest(
client.accessToken,
http.MethodGet,
endpoint,
"",
query,
nil,
beta,
)
if err != nil {
return nil, fmt.Errorf("list %s: %w", resourceName, err)
}

var page catalogPage[T]
if err := json.Unmarshal(response, &page); err != nil {
return nil, fmt.Errorf("decode %s response: %w", resourceName, err)
}

items = append(items, page.Items...)
if len(page.Items) < catalogPageLimit ||
(page.TotalCount > 0 && len(items) >= page.TotalCount) {
return items, nil
}
}
}
Loading
Loading