Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f394728
REL-000: skeleton for the CLI
ari-launchdarkly May 12, 2026
b490e4e
make the suggested changes
ari-launchdarkly May 13, 2026
0405325
make the suggested bugbot changes
ari-launchdarkly May 13, 2026
bfdfe48
address more bugbot comments
ari-launchdarkly May 13, 2026
9bad6ba
make the suggested changes
ari-launchdarkly May 13, 2026
c0f9fec
[REL-13574] sdk detection and installation
dakotasanchez May 12, 2026
0671b01
Update internal/setup/detector.go
dakotasanchez May 13, 2026
5cacdae
Update internal/setup/detector.go
dakotasanchez May 13, 2026
11088f2
Update internal/setup/installer.go
dakotasanchez May 13, 2026
2498bbb
Update internal/setup/installer.go
dakotasanchez May 13, 2026
3593f84
[REL-13574] feedback
dakotasanchez May 13, 2026
ce86df7
[REL-13574] change to always show (prioritized) SDK selector
dakotasanchez May 13, 2026
a344e11
[REL-13574] lingering bugs
dakotasanchez May 13, 2026
f243727
[REL-13574] merge resolution and new go code injection
dakotasanchez May 14, 2026
e33be9f
[REL-13574] fix panic and flag link
dakotasanchez May 14, 2026
96a454c
Fix setup wizard review findings: init placement, ruby detect, dry-run
ffantl-ld Jul 16, 2026
341b524
setup: pre-flight auth check with login guidance
ffantl-ld Jul 16, 2026
9e81930
setup: friendlier not-logged-in copy and detected-SDK prompt
ffantl-ld Jul 17, 2026
537b813
setup: dim the transition notice and separate it from output
ffantl-ld Jul 17, 2026
58b888b
setup: split SDK selection into detected panel and other-SDKs list
ffantl-ld Jul 17, 2026
f0e7950
setup: show "Press q to quit" on the final screens
ffantl-ld Jul 17, 2026
ab2b416
setup: point to the detected SDK when its panel is focused
ffantl-ld Jul 17, 2026
f0a7c2b
setup: align SDK boxes and dim the unfocused list selection
ffantl-ld Jul 17, 2026
71f5b9b
setup: skip install when the SDK is already a dependency
ffantl-ld Jul 17, 2026
25ae37e
setup: preview the steps and confirm before running them
ffantl-ld Jul 17, 2026
05d70c0
setup: clean up the not-logged-in message
ffantl-ld Jul 17, 2026
cf5bba8
setup: centralize TUI styles into shared tokens
ffantl-ld Jul 17, 2026
183831b
setup: match the SDK list title style to the panel header
ffantl-ld Jul 17, 2026
e5fbed4
setup: go back to a previous selection with the left arrow
ffantl-ld Jul 17, 2026
a19b1c1
setup: detect the project once at launch and cache it
ffantl-ld Jul 17, 2026
b56b103
setup: single key hint in the SDK box and vim `h` for back
ffantl-ld Jul 21, 2026
404be45
setup: graceful install fallback, manual-SDK marking, catalog link, w…
ffantl-ld Jul 21, 2026
7abe7ed
setup: render init snippets and commands as code blocks
ffantl-ld Jul 21, 2026
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
44 changes: 35 additions & 9 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ import (
flagscmd "github.com/launchdarkly/ldcli/cmd/flags"
logincmd "github.com/launchdarkly/ldcli/cmd/login"
memberscmd "github.com/launchdarkly/ldcli/cmd/members"
sdkactivecmd "github.com/launchdarkly/ldcli/cmd/sdk_active"
resourcecmd "github.com/launchdarkly/ldcli/cmd/resources"
sdkactivecmd "github.com/launchdarkly/ldcli/cmd/sdk_active"
setupcmd "github.com/launchdarkly/ldcli/cmd/setup"
signupcmd "github.com/launchdarkly/ldcli/cmd/signup"
sourcemapscmd "github.com/launchdarkly/ldcli/cmd/sourcemaps"
whoamicmd "github.com/launchdarkly/ldcli/cmd/whoami"
Expand All @@ -36,6 +37,7 @@ import (
"github.com/launchdarkly/ldcli/internal/members"
"github.com/launchdarkly/ldcli/internal/projects"
"github.com/launchdarkly/ldcli/internal/resources"
"github.com/launchdarkly/ldcli/internal/setup"
)

type APIClients struct {
Expand All @@ -45,6 +47,8 @@ type APIClients struct {
MembersClient members.Client
ProjectsClient projects.Client
ResourcesClient resources.Client
Detector setup.Detector
Installer setup.Installer
}

type Command interface {
Expand Down Expand Up @@ -125,23 +129,26 @@ func NewRootCommand(
Long: "LaunchDarkly CLI to control your feature flags",
Version: version,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
// disable required flags when running certain commands
// Skip the global --access-token required-flag check for commands
// that don't need an API token. We clear the annotation on the
// specific flag instead of setting DisableFlagParsing, which would
// also suppress validation for the subcommand's own required flags.
for _, name := range []string{
"completion",
"config",
"help",
"login",
"setup",
"signup",
"whoami",
} {
if cmd.HasParent() && cmd.Parent().Name() == name {
cmd.DisableFlagParsing = true
}
if cmd.Name() == name {
cmd.DisableFlagParsing = true
if cmd.Name() == name || (cmd.HasParent() && cmd.Parent().Name() == name) {
if f := cmd.Flags().Lookup(cliflags.AccessTokenFlag); f != nil {
delete(f.Annotations, cobra.BashCompOneRequiredFlag)
}
break
}
}

},
Annotations: make(map[string]string),
// Handle errors differently based on type.
Expand Down Expand Up @@ -253,7 +260,26 @@ func NewRootCommand(

configCmd := configcmd.NewConfigCmd(configService, analyticsTrackerFn)
cmd.AddCommand(configCmd.Cmd())
cmd.AddCommand(NewQuickStartCmd(analyticsTrackerFn, clients.EnvironmentsClient, clients.FlagsClient))
detector := clients.Detector
if detector == nil {
detector = setup.FileDetector{}
}
installer := clients.Installer
if installer == nil {
installer = setup.PackageInstaller{}
}
cmd.AddCommand(setupcmd.NewSetupCmd(
analyticsTrackerFn,
clients.ResourcesClient,
clients.FlagsClient,
detector,
installer,
))
quickStartCmd := NewQuickStartCmd(analyticsTrackerFn, clients.EnvironmentsClient, clients.FlagsClient)
quickStartCmd.Use = "quickstart"
quickStartCmd.Hidden = true
quickStartCmd.Deprecated = "use 'ldcli setup' for the new guided setup experience"
cmd.AddCommand(quickStartCmd)
cmd.AddCommand(logincmd.NewLoginCmd(clients.ResourcesClient))
cmd.AddCommand(signupcmd.NewSignupCmd(analyticsTrackerFn))
cmd.AddCommand(resourcecmd.NewResourcesCmd())
Expand Down
62 changes: 62 additions & 0 deletions cmd/setup/detect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package setup

import (
"encoding/json"
"fmt"
"os"

"github.com/spf13/cobra"

"github.com/launchdarkly/ldcli/cmd/cliflags"
"github.com/launchdarkly/ldcli/internal/setup"
)

const pathFlag = "path"

func newDetectCmd(detector setup.Detector) *cobra.Command {
cmd := &cobra.Command{
Use: "detect",
Short: "Detect language, framework, and recommended SDK for a project",
Hidden: true,
RunE: runDetect(detector),
}

cmd.Flags().String(pathFlag, "", "Path to the project directory (defaults to current directory)")

return cmd
}

func runDetect(detector setup.Detector) func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, args []string) error {
dir, _ := cmd.Flags().GetString(pathFlag)
if dir == "" {
var err error
dir, err = os.Getwd()
if err != nil {
return err
}
}

result, err := detector.Detect(dir)
if err != nil {
return err
}

outputKind := cliflags.GetOutputKind(cmd)
if outputKind == "json" {
data, _ := json.Marshal(result)
fmt.Fprintln(cmd.OutOrStdout(), string(data))
return nil
}

fmt.Fprintf(cmd.OutOrStdout(), "Language: %s\n", result.Language)
if result.Framework != "" {
fmt.Fprintf(cmd.OutOrStdout(), "Framework: %s\n", result.Framework)
}
fmt.Fprintf(cmd.OutOrStdout(), "Package Manager: %s\n", result.PackageManager)
fmt.Fprintf(cmd.OutOrStdout(), "Recommended SDK: %s\n", result.SDKID)
fmt.Fprintf(cmd.OutOrStdout(), "Entry Point: %s\n", result.EntryPoint)

return nil
}
}
86 changes: 86 additions & 0 deletions cmd/setup/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package setup

import (
"encoding/json"
"fmt"

"github.com/spf13/cobra"

"github.com/launchdarkly/ldcli/cmd/cliflags"
"github.com/launchdarkly/ldcli/internal/setup"
)

func getFlag(cmd *cobra.Command, name string) string {
v, _ := cmd.Flags().GetString(name)
return v
}

const (
fileFlag = "file"
sdkKeyFlag = "sdk-key"
clientIDFlag = "client-side-id"
mobileFlag = "mobile-key"
flagKeyFlag = "flag-key"
)

func newInitCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "init",
Short: "Inject LaunchDarkly SDK initialization code into a file",
Hidden: true,
RunE: runInit(),
}

cmd.Flags().String(sdkIDFlag, "", "SDK identifier (e.g. node-server, react-client-sdk)")
_ = cmd.MarkFlagRequired(sdkIDFlag)

cmd.Flags().String(fileFlag, "", "Target file to inject initialization code into")
_ = cmd.MarkFlagRequired(fileFlag)

cmd.Flags().String(sdkKeyFlag, "", "Server-side SDK key")
cmd.Flags().String(clientIDFlag, "", "Client-side environment ID")
cmd.Flags().String(mobileFlag, "", "Mobile SDK key")
cmd.Flags().String(flagKeyFlag, "", "Feature flag key to use in the initialization example")

return cmd
}

func runInit() func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, args []string) error {
sdkID, _ := cmd.Flags().GetString(sdkIDFlag)
filePath, _ := cmd.Flags().GetString(fileFlag)
cfg := setup.InitConfig{
SDKKey: getFlag(cmd, sdkKeyFlag),
ClientSideID: getFlag(cmd, clientIDFlag),
MobileKey: getFlag(cmd, mobileFlag),
FlagKey: getFlag(cmd, flagKeyFlag),
}

initializer := setup.Initializer{}
result, err := initializer.InjectIntoFile(sdkID, filePath, cfg)
if err != nil {
return err
}

outputKind := cliflags.GetOutputKind(cmd)
if outputKind == "json" {
data, _ := json.Marshal(result)
fmt.Fprintln(cmd.OutOrStdout(), string(data))
return nil
}

if !result.Success {
if result.Snippet != "" {
fmt.Fprintf(cmd.OutOrStdout(), "Manual setup required for %s — add the following to %s:\n\n%s\n\n", result.SDKID, result.FilePath, result.Snippet)
fmt.Fprintf(cmd.OutOrStdout(), "Follow the setup guide at: %s\n", result.DocsURL)
return nil
}
fmt.Fprintf(cmd.OutOrStdout(), "No initialization template available for %s\n", result.SDKID)
fmt.Fprintf(cmd.OutOrStdout(), "Follow the setup guide at: %s\n", result.DocsURL)
return nil
}

fmt.Fprintf(cmd.OutOrStdout(), "Injected %s initialization into %s\n", result.SDKID, result.FilePath)
return nil
}
}
99 changes: 99 additions & 0 deletions cmd/setup/install.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package setup

import (
"encoding/json"
"fmt"
"os"
"strings"

"github.com/spf13/cobra"

"github.com/launchdarkly/ldcli/cmd/cliflags"
"github.com/launchdarkly/ldcli/internal/setup"
)

const (
sdkIDFlag = "sdk-id"
dryRunFlag = "dry-run"
)

func newInstallCmd(installer setup.Installer) *cobra.Command {
cmd := &cobra.Command{
Use: "install",
Short: "Install the LaunchDarkly SDK package for the detected project",
Hidden: true,
RunE: runInstall(installer),
}

cmd.Flags().String(pathFlag, "", "Path to the project directory (defaults to current directory)")
cmd.Flags().String(sdkIDFlag, "", "SDK identifier to install (e.g. node-server, react-client-sdk)")
_ = cmd.MarkFlagRequired(sdkIDFlag)
cmd.Flags().String("package-manager", "", "Package manager to use (e.g. npm, pip, go)")
cmd.Flags().Bool(dryRunFlag, false, "Print the install command that would run without executing it")

return cmd
}

func runInstall(installer setup.Installer) func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, args []string) error {
dir, _ := cmd.Flags().GetString(pathFlag)
if dir == "" {
var err error
dir, err = os.Getwd()
if err != nil {
return err
}
}

sdkID, _ := cmd.Flags().GetString(sdkIDFlag)
pkgMgr, _ := cmd.Flags().GetString("package-manager")
dryRun, _ := cmd.Flags().GetBool(dryRunFlag)
detection := &setup.DetectResult{
SDKID: sdkID,
PackageManager: pkgMgr,
}

var result *setup.InstallResult
if dryRun {
args, pkg := setup.InstallArgs(sdkID, pkgMgr)
result = &setup.InstallResult{
SDKID: sdkID,
Package: pkg,
Command: strings.Join(args, " "),
DryRun: true,
}
} else {
var err error
result, err = installer.Install(dir, detection)
if err != nil {
return err
}
}

outputKind := cliflags.GetOutputKind(cmd)
if outputKind == "json" {
data, _ := json.Marshal(result)
fmt.Fprintln(cmd.OutOrStdout(), string(data))
return nil
}

fmt.Fprintf(cmd.OutOrStdout(), "SDK: %s\n", result.SDKID)
if result.Version != "" {
fmt.Fprintf(cmd.OutOrStdout(), "Package: %s@%s\n", result.Package, result.Version)
} else {
fmt.Fprintf(cmd.OutOrStdout(), "Package: %s\n", result.Package)
}
if result.AlreadyInstalled {
fmt.Fprintln(cmd.OutOrStdout(), "Already installed — skipping install.")
return nil
}
fmt.Fprintf(cmd.OutOrStdout(), "Command: %s\n", result.Command)
if result.DryRun {
fmt.Fprintln(cmd.OutOrStdout(), "Dry run: command not executed")
} else {
fmt.Fprintf(cmd.OutOrStdout(), "Success: %t\n", result.Success)
}

return nil
}
}
54 changes: 54 additions & 0 deletions cmd/setup/setup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package setup

import (
"fmt"

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

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

// NewSetupCmd creates the top-level setup command and registers its hidden subcommands.
func NewSetupCmd(
analyticsTrackerFn analytics.TrackerFn,
resourcesClient resources.Client,
flagsClient flags.Client,
detector setup.Detector,
installer setup.Installer,
) *cobra.Command {
cmd := &cobra.Command{
Use: "setup",
Short: "Set up LaunchDarkly in your project",
Long: `Guided setup to integrate LaunchDarkly into your codebase.

Detects your project's language and framework, installs the correct SDK,
initializes it with your environment's SDK key, creates a feature flag,
and verifies the connection.`,
PreRun: func(cmd *cobra.Command, args []string) {
// Dim the notice and set it off with a blank line so it reads as a
// transitional notice, visually distinct from command output.
notice := mutedStyle.Render(
"Notice: 'ldcli setup' now runs the new guided setup wizard (project detection, SDK installation, and initialization).\n" +
"The previous quickstart wizard is still available via 'ldcli quickstart' during the transition period.")
fmt.Fprintf(cmd.ErrOrStderr(), "%s\n\n", notice)
analyticsTrackerFn(
viper.GetString(cliflags.AccessTokenFlag),
viper.GetString(cliflags.BaseURIFlag),
viper.GetBool(cliflags.AnalyticsOptOut),
).SendCommandRunEvent(cmdAnalytics.CmdRunEventProperties(cmd, "setup", nil))
},
RunE: runSetupWizard(analyticsTrackerFn, resourcesClient, flagsClient, detector, installer),
}

cmd.AddCommand(newDetectCmd(detector))
cmd.AddCommand(newInstallCmd(installer))
cmd.AddCommand(newInitCmd())

return cmd
}
Loading
Loading