Skip to content
Draft
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
72 changes: 72 additions & 0 deletions cmd/onboard.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package cmd

import (
"fmt"

"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/cmd/validators"
"github.com/launchdarkly/ldcli/internal/analytics"
"github.com/launchdarkly/ldcli/internal/onboarding"
)

func NewOnboardCmd(
analyticsTrackerFn analytics.TrackerFn,
) *cobra.Command {
cmd := &cobra.Command{
Args: validators.Validate(),
Long: "Generate an AI-agent-compatible onboarding plan for integrating the LaunchDarkly SDK into your project. The output is a structured workflow of agent-skills that guide detection, installation, validation, and first flag creation.",
Short: "Generate an AI-agent onboarding plan for SDK integration",
Use: "onboard",
PreRun: func(cmd *cobra.Command, args []string) {
analyticsTrackerFn(
viper.GetString(cliflags.AccessTokenFlag),
viper.GetString(cliflags.BaseURIFlag),
viper.GetBool(cliflags.AnalyticsOptOut),
).SendCommandRunEvent(cmdAnalytics.CmdRunEventProperties(cmd, "onboard", nil))
},
RunE: runOnboard(),
}

cmd.SetUsageTemplate(resourcescmd.SubcommandUsageTemplate())
initOnboardFlags(cmd)

return cmd
}

func runOnboard() func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, args []string) error {
baseURI := viper.GetString(cliflags.BaseURIFlag)
project := viper.GetString(cliflags.ProjectFlag)
environment := viper.GetString(cliflags.EnvironmentFlag)

plan := onboarding.BuildPlan(baseURI, project, environment)

outputKind := cliflags.GetOutputKind(cmd)
if outputKind == "json" {
data, err := plan.ToJSON()
if err != nil {
return err
}
fmt.Fprintln(cmd.OutOrStdout(), string(data))
return nil
}

fmt.Fprintln(cmd.OutOrStdout(), plan.Plaintext())
return nil
}
}

func initOnboardFlags(cmd *cobra.Command) {
cmd.Flags().String(cliflags.ProjectFlag, "default", "The project key")
_ = cmd.Flags().SetAnnotation(cliflags.ProjectFlag, "required", []string{"true"})
_ = viper.BindPFlag(cliflags.ProjectFlag, cmd.Flags().Lookup(cliflags.ProjectFlag))

cmd.Flags().String(cliflags.EnvironmentFlag, "test", "The environment key")
_ = cmd.Flags().SetAnnotation(cliflags.EnvironmentFlag, "required", []string{"true"})
_ = viper.BindPFlag(cliflags.EnvironmentFlag, cmd.Flags().Lookup(cliflags.EnvironmentFlag))
}
77 changes: 77 additions & 0 deletions cmd/onboard_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package cmd_test

import (
"testing"

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

"github.com/launchdarkly/ldcli/cmd"
"github.com/launchdarkly/ldcli/internal/analytics"
)

func TestOnboard(t *testing.T) {
t.Run("with JSON output returns valid JSON plan", func(t *testing.T) {
args := []string{
"onboard",
"--access-token", "test-token",
"--project", "default",
"--environment", "test",
"--output", "json",
}

output, err := cmd.CallCmd(
t,
cmd.APIClients{},
analytics.NoopClientFn{}.Tracker(),
args,
)

require.NoError(t, err)
assert.Contains(t, string(output), `"steps"`)
assert.Contains(t, string(output), `"sdk_recipes"`)
assert.Contains(t, string(output), `"recovery_options"`)
assert.Contains(t, string(output), `"detect"`)
})

t.Run("with plaintext output returns readable summary", func(t *testing.T) {
args := []string{
"onboard",
"--access-token", "test-token",
"--project", "default",
"--environment", "test",
}

output, err := cmd.CallCmd(
t,
cmd.APIClients{},
analytics.NoopClientFn{}.Tracker(),
args,
)

require.NoError(t, err)
assert.Contains(t, string(output), "LaunchDarkly AI-Agent Onboarding Plan")
assert.Contains(t, string(output), "Detect Repository Stack")
assert.Contains(t, string(output), "--json")
})

t.Run("with --json flag returns JSON output", func(t *testing.T) {
args := []string{
"onboard",
"--access-token", "test-token",
"--project", "default",
"--environment", "test",
"--json",
}

output, err := cmd.CallCmd(
t,
cmd.APIClients{},
analytics.NoopClientFn{}.Tracker(),
args,
)

require.NoError(t, err)
assert.Contains(t, string(output), `"steps"`)
})
}
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ func NewRootCommand(

configCmd := configcmd.NewConfigCmd(configService, analyticsTrackerFn)
cmd.AddCommand(configCmd.Cmd())
cmd.AddCommand(NewOnboardCmd(analyticsTrackerFn))
cmd.AddCommand(NewQuickStartCmd(analyticsTrackerFn, clients.EnvironmentsClient, clients.FlagsClient))
cmd.AddCommand(logincmd.NewLoginCmd(clients.ResourcesClient))
cmd.AddCommand(signupcmd.NewSignupCmd(analyticsTrackerFn))
Expand Down
1 change: 1 addition & 0 deletions cmd/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ func getUsageTemplate() string {
{{.CommandPath}} [command]

Commands:
{{rpad "onboard" 29}} Generate an AI-agent onboarding plan for SDK integration
{{rpad "setup" 29}} Create your first feature flag using a step-by-step guide
{{rpad "config" 29}} View and modify specific configuration values
{{rpad "completion" 29}} Enable command autocompletion within supported shells
Expand Down
118 changes: 118 additions & 0 deletions internal/onboarding/onboarding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package onboarding

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

// Step represents a single step in the onboarding workflow that an AI agent should execute.
type Step struct {
ID string `json:"id"`
Title string `json:"title"`
Instructions string `json:"instructions"`
Tools []string `json:"tools"`
SuccessCriteria string `json:"success_criteria"`
Next string `json:"next"`
OnFailure string `json:"on_failure"`
}

// RecoveryOption represents a ranked recovery action when a step fails.
type RecoveryOption struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Priority int `json:"priority"`
}

// OnboardingPlan is the top-level structure returned by the onboard command,
// providing the AI agent with a complete set of instructions.
type OnboardingPlan struct {
Steps []Step `json:"steps"`
SDKRecipes []SDKRecipe `json:"sdk_recipes"`
RecoveryOptions []RecoveryOption `json:"recovery_options"`
}

// ToJSON serializes the onboarding plan to indented JSON.
func (p OnboardingPlan) ToJSON() ([]byte, error) {
return json.MarshalIndent(p, "", " ")
}

// Plaintext returns a human-readable summary of the onboarding plan.
func (p OnboardingPlan) Plaintext() string {
var sb strings.Builder
sb.WriteString("LaunchDarkly AI-Agent Onboarding Plan\n")
sb.WriteString("=====================================\n\n")

for i, step := range p.Steps {
sb.WriteString(fmt.Sprintf("Step %d: %s\n", i+1, step.Title))
sb.WriteString(fmt.Sprintf(" ID: %s\n", step.ID))
sb.WriteString(fmt.Sprintf(" Success Criteria: %s\n", step.SuccessCriteria))
if step.Next != "" {
sb.WriteString(fmt.Sprintf(" Next: %s\n", step.Next))
}
sb.WriteString("\n")
}

sb.WriteString(fmt.Sprintf("SDK Recipes: %d available\n", len(p.SDKRecipes)))
sb.WriteString(fmt.Sprintf("Recovery Options: %d defined\n\n", len(p.RecoveryOptions)))
sb.WriteString("Run with --json for the full structured plan suitable for AI agent consumption.\n")

return sb.String()
}

// BuildPlan constructs the full onboarding plan with all steps, SDK recipes, and recovery options.
func BuildPlan(baseURI, project, environment string) OnboardingPlan {
return OnboardingPlan{
Steps: buildSteps(baseURI, project, environment),
SDKRecipes: AllSDKRecipes(),
RecoveryOptions: buildRecoveryOptions(),
}
}

func buildRecoveryOptions() []RecoveryOption {
return []RecoveryOption{
{
ID: "retry-step",
Title: "Retry Current Step",
Description: "Re-attempt the current step after reviewing the error output.",
Priority: 1,
},
{
ID: "check-credentials",
Title: "Verify Credentials",
Description: "Confirm the LaunchDarkly access token is valid and has write-level access. Run: ldcli environments list --project <project>",
Priority: 2,
},
{
ID: "check-network",
Title: "Check Network Connectivity",
Description: "Verify the application can reach LaunchDarkly endpoints. Check firewall rules and proxy settings.",
Priority: 3,
},
{
ID: "manual-install",
Title: "Manual SDK Install",
Description: "If automatic dependency installation fails, provide the user with copy/paste instructions from the SDK recipe.",
Priority: 4,
},
{
ID: "switch-sdk",
Title: "Try Alternative SDK",
Description: "If detection chose the wrong SDK, re-run the detect step or allow the user to manually select an SDK.",
Priority: 5,
},
{
ID: "skip-step",
Title: "Skip to Next Step",
Description: "If a step is non-critical (e.g., auto-run failed but user can start manually), skip to the next step.",
Priority: 6,
},
{
ID: "abort",
Title: "Abort Onboarding",
Description: "Stop the onboarding workflow entirely and present the user with documentation links for manual setup.",
Priority: 7,
},
}
}
Loading
Loading