Skip to content
Merged
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
2 changes: 2 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
signupcmd "github.com/launchdarkly/ldcli/cmd/signup"
sourcemapscmd "github.com/launchdarkly/ldcli/cmd/sourcemaps"
symbolscmd "github.com/launchdarkly/ldcli/cmd/symbols"
synccmd "github.com/launchdarkly/ldcli/cmd/sync"
whoamicmd "github.com/launchdarkly/ldcli/cmd/whoami"
"github.com/launchdarkly/ldcli/internal/analytics"
"github.com/launchdarkly/ldcli/internal/config"
Expand Down Expand Up @@ -301,6 +302,7 @@ func NewRootCommand(
cmd.AddCommand(devcmd.NewDevServerCmd(clients.ResourcesClient, analyticsTrackerFn, clients.DevClient))
cmd.AddCommand(sourcemapscmd.NewSourcemapsCmd(clients.ResourcesClient, analyticsTrackerFn))
cmd.AddCommand(symbolscmd.NewSymbolsCmd(clients.ResourcesClient, analyticsTrackerFn))
cmd.AddCommand(synccmd.NewSyncCmd(clients.ResourcesClient, analyticsTrackerFn))
cmd.AddCommand(whoamicmd.NewWhoAmICmd(clients.ResourcesClient))
resourcecmd.AddAllResourceCmds(cmd, clients.ResourcesClient, analyticsTrackerFn)

Expand Down
1 change: 1 addition & 0 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ func TestNewRootCommand_RegistersTopLevelCommands(t *testing.T) {
"signup",
"sourcemaps",
"symbols",
"sync",
"whoami",
} {
assert.True(t, registered[name], "%s is not registered on the root command", name)
Expand Down
94 changes: 94 additions & 0 deletions cmd/sync/prompt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package sync

import (
"fmt"
"os"

"github.com/spf13/cobra"
"github.com/spf13/viper"

"github.com/launchdarkly/ldcli/cmd/cliflags"
resourcescmd "github.com/launchdarkly/ldcli/cmd/resources"
"github.com/launchdarkly/ldcli/cmd/validators"
"github.com/launchdarkly/ldcli/internal/output"
"github.com/launchdarkly/ldcli/internal/resources"
syncprompt "github.com/launchdarkly/ldcli/internal/sync/prompt"
)

const (
addFlag = "add"
detachFlag = "detach"
dryRunFlag = "dry-run"
formatFlag = "format"
linkFlag = "link"
watchFlag = "watch"
yesFlag = "yes"
)

// NewPromptCmd creates the prompt synchronization command.
func NewPromptCmd(client resources.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "prompt",
Short: "Synchronize local prompt variations with LaunchDarkly",
Long: "Bootstrap local prompt variations from LaunchDarkly, add more variations, or synchronize changes using the committed local manifest. " +
"Sync rechecks state before every write, rerun sync after a change.",
Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.NoArgs(cmd, args); err != nil {
return err
}
return validators.Validate()(cmd, args)
},
RunE: runPrompt(client),
}

cmd.Flags().Bool(addFlag, false, "Select additional prompt variations from LaunchDarkly")
cmd.Flags().Bool(detachFlag, false, "Select local resources to stop syncing")
cmd.Flags().Bool(dryRunFlag, false, "Preview synchronization changes without applying them")
cmd.Flags().String(linkFlag, "", "Link an external prompt file")
cmd.Flags().String(formatFlag, "", "Format adapter for --link (for example, plain-markdown)")
cmd.Flags().Bool(watchFlag, false, "Sync when managed or referenced files change")
cmd.Flags().Bool(yesFlag, false, "Apply synchronization changes without interactive confirmation")
cmd.SetUsageTemplate(resourcescmd.SubcommandUsageTemplate())

return cmd
}

func runPrompt(client resources.Client) func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, _ []string) error {
workingDirectory, err := os.Getwd()
if err != nil {
return fmt.Errorf("get working directory: %w", err)
}

add, _ := cmd.Flags().GetBool(addFlag)
detach, _ := cmd.Flags().GetBool(detachFlag)
dryRun, _ := cmd.Flags().GetBool(dryRunFlag)
format, _ := cmd.Flags().GetString(formatFlag)
link, _ := cmd.Flags().GetString(linkFlag)
watch, _ := cmd.Flags().GetBool(watchFlag)
yes, _ := cmd.Flags().GetBool(yesFlag)
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,
Detach: detach,
DryRun: dryRun,
Format: format,
Link: link,
Watch: watch,
Yes: yes,
Context: cmd.Context(),
Input: cmd.InOrStdin(),
Output: cmd.OutOrStdout(),
ErrorOutput: cmd.ErrOrStderr(),
})
if err != nil {
return output.NewCmdOutputError(err, outputKind)
}
return nil
}
}
18 changes: 18 additions & 0 deletions cmd/sync/prompt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package sync

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestPromptCommandDefinesSyncFlags(t *testing.T) {
command := NewPromptCmd(nil)

assert.Equal(t, "prompt", command.Use)
for _, name := range []string{addFlag, detachFlag, dryRunFlag, formatFlag, linkFlag, watchFlag, yesFlag} {
assert.NotNil(t, command.Flags().Lookup(name), "missing --%s", name)
}
assert.Nil(t, command.Flags().Lookup("apply"))
assert.Nil(t, command.Flags().Lookup("project"))
}
38 changes: 38 additions & 0 deletions cmd/sync/sync.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package sync

import (
"github.com/spf13/cobra"
"github.com/spf13/viper"

cmdAnalytics "github.com/launchdarkly/ldcli/cmd/analytics"
"github.com/launchdarkly/ldcli/cmd/cliflags"
resourcescmd "github.com/launchdarkly/ldcli/cmd/resources"
"github.com/launchdarkly/ldcli/internal/analytics"
"github.com/launchdarkly/ldcli/internal/resources"
)

// NewSyncCmd creates the parent command for synchronization workflows.
func NewSyncCmd(client resources.Client, analyticsTrackerFn analytics.TrackerFn) *cobra.Command {
cmd := &cobra.Command{
Use: "sync",
Short: "Synchronize local resources with LaunchDarkly",
Args: cobra.MinimumNArgs(1),
PersistentPreRun: func(cmd *cobra.Command, _ []string) {
tracker := analyticsTrackerFn(
viper.GetString(cliflags.AccessTokenFlag),
viper.GetString(cliflags.BaseURIFlag),
viper.GetBool(cliflags.AnalyticsOptOut),
)
tracker.SendCommandRunEvent(cmdAnalytics.CmdRunEventProperties(
cmd,
"sync",
map[string]interface{}{"action": cmd.Name()},
))
},
}

cmd.AddCommand(NewPromptCmd(client))
cmd.SetUsageTemplate(resourcescmd.SubcommandUsageTemplate())

return cmd
}
1 change: 1 addition & 0 deletions cmd/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Commands:
{{rpad "login" 29}} Log in to your LaunchDarkly account
{{rpad "signup" 29}} Create a new LaunchDarkly account
{{rpad "dev-server" 29}} Run a development server to serve flags locally
{{rpad "sync" 29}} Synchronize local resources with LaunchDarkly

Common resource commands:
{{rpad "flags" 29}} List, create, and modify feature flags and their targeting
Expand Down
1 change: 1 addition & 0 deletions cmd/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func TestGetUsageTemplate_ListsTopLevelCommands(t *testing.T) {
"login",
"signup",
"dev-server",
"sync",
"flags",
"environments",
"projects",
Expand Down
5 changes: 3 additions & 2 deletions internal/sync/interactive/form.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ func choiceLabel[T any](choice Choice[T]) string {
// formTheme applies the subdued sync palette consistently to every form.
func formTheme() *huh.Theme {
theme := huh.ThemeBase()
accent := lipgloss.Color("170")
accent := lipgloss.AdaptiveColor{Light: "24", Dark: "67"}
buttonText := lipgloss.AdaptiveColor{Light: "255", Dark: "0"}

theme.Focused.Title = theme.Focused.Title.Foreground(accent).Bold(true)
theme.Focused.SelectSelector = theme.Focused.SelectSelector.Foreground(accent)
Expand All @@ -152,7 +153,7 @@ func formTheme() *huh.Theme {
theme.Focused.SelectedPrefix = theme.Focused.SelectedPrefix.Foreground(accent)
theme.Focused.FocusedButton = theme.Focused.FocusedButton.
Background(accent).
Foreground(lipgloss.Color("0"))
Foreground(buttonText)

return theme
}
Loading
Loading