Skip to content

[REL-13574] Implement SDK detection and installation#709

Open
dakotasanchez wants to merge 8 commits into
ari-launchdarkly/REL-0000-clifrom
dsanchez/REL-13574/cli
Open

[REL-13574] Implement SDK detection and installation#709
dakotasanchez wants to merge 8 commits into
ari-launchdarkly/REL-0000-clifrom
dsanchez/REL-13574/cli

Conversation

@dakotasanchez
Copy link
Copy Markdown

@dakotasanchez dakotasanchez commented May 12, 2026

Summary

Implements the Detector and Installer interfaces that were stubbed in the base branch, wiring up real filesystem-based SDK detection and package-manager-based installation into the ldcli setup wizard and hidden subcommands.

  • FileDetector — scans the working directory for project indicators and returns language, framework, package manager, recommended SDK ID, and entry point file
  • PackageInstaller — maps SDK IDs to their install commands and shells out to the appropriate package manager
  • APIClients gains Detector and Installer fields for test injection, with FileDetector/PackageInstaller as production defaults
  • Fixed install.go output: Package: pkg@version was printing a trailing @ when Version is empty
  • Fixed cmd/root.go import ordering (gofmt)
  • Manual-install SDKs (Java, Android, Swift) now return meaningful package identifiers (com.launchdarkly:launchdarkly-java-server-sdk etc.) rather than the raw SDK ID

How SDK Detection and Installation Work

Detection (FileDetector)

FileDetector.Detect(dir) runs four probes in priority order against the project directory:

  1. Node.js / React / Next.js — reads package.json and inspects dependencies + devDependencies:
    • Has nextnode-server SDK, framework = "Next.js"
    • Has reactreact-client-sdk, framework = "React"
    • Otherwise → node-server (plain Node)
    • Package manager inferred from lock files: pnpm-lock.yaml → pnpm, yarn.lock → yarn, default → npm
  2. Go — presence of go.modgo-server-sdk
  3. Python — presence of requirements.txt, pyproject.toml, or setup.pypython-server-sdk
  4. Java — presence of pom.xml (→ mvn) or build.gradle/build.gradle.kts (→ gradle) → java-server-sdk

If nothing matches, returns an error; the wizard surfaces it and halts.

Entry point detection walks a prioritized candidate list per SDK (e.g. for React: src/App.tsxsrc/App.jsxsrc/index.tsx → ... → index.js) and returns the first path that exists on disk, or the last candidate as a suggested path if none do. The returned path is absolute and passed directly to Initializer.InjectIntoFile.

Installation (PackageInstaller)

PackageInstaller.Install(dir, detectResult) calls InstallArgs(sdkID, packageManager) which maps each SDK to its install command:

SDK Command
react-client-sdk npm/yarn/pnpm install launchdarkly-react-client-sdk
node-server npm/yarn/pnpm install @launchdarkly/node-server-sdk
python-server-sdk pip install launchdarkly-server-sdk
go-server-sdk go get github.com/launchdarkly/go-server-sdk/v7
ruby-server-sdk gem install launchdarkly-server-sdk
dotnet-server-sdk dotnet add package LaunchDarkly.ServerSdk
Java / Android / Swift no command — Success: false, no error

For SDKs with a command, it shells out via exec.Command in the project directory and captures combined output. A non-zero exit wraps the error and output into the returned error. For SDKs without a command, it returns Success: false with no error so the wizard proceeds (the user still needs to add the dependency manually, but flag creation and code injection remain valid).

Wizard Integration

selectProject → selectEnvironment → fetchEnvDetails
                                          │
                                          ▼
                                      stepDetect  ←── FileDetector.Detect(cwd)
                                          │              → SDKID, PackageManager, EntryPoint
                                          ▼
                                      stepInstall ←── PackageInstaller.Install(cwd, detectResult)
                                          │              → shells out: npm install / go get / pip install / ...
                                          ▼
                                      stepCreateFlag → stepInit → stepWaitForApp → stepVerify → stepDone

detectResult.SDKID flows into both the installer (command selection) and the initializer (template selection). detectResult.EntryPoint is passed directly to InjectIntoFile. All five SDKs the detector can return have init templates, so the stepWaitForApp file path display is always valid.

Test plan

  • go build ./... passes
  • go test ./internal/setup/... — 26 tests covering all detection scenarios, package managers, entry point fallback, firstExistingIn edge cases, InstallArgs for all SDK types, and PackageInstaller with mock runner
  • go test ./cmd/setup/... — detect error/success/JSON, install error/success/JSON/version paths
  • go test ./... — all 28 packages pass
  • gofmt clean on all changed files
  • golangci-lint — no new issues introduced (6 pre-existing errcheck findings in ari's files)
  • Manual: ldcli setup detect --path /path/to/react/project returns language/framework/SDK info
  • Manual: ldcli setup detect --path /tmp/empty returns "could not detect" error
  • Manual: ldcli setup install --sdk-id node-server runs npm install @launchdarkly/node-server-sdk
  • Manual: ldcli setup install --sdk-id java-server-sdk returns Success: false with package com.launchdarkly:launchdarkly-java-server-sdk

Requirements

  • I have added test coverage for new or changed functionality
  • I have followed the repository's pull request submission guidelines
  • I have validated my changes against all supported platform versions

Note

Medium Risk
Adds real project detection and shells out to system package managers during ldcli setup, which can affect local environments and error handling. Wizard flow changes (manual SDK selection fallback) introduce new state transitions but are covered by tests.

Overview
ldcli setup now uses real implementations for SDK detection and installation: a new filesystem-based FileDetector infers language/framework/package manager/entry point from common project files, and PackageInstaller maps SDK IDs to install commands and runs them via the system package manager.

The setup wizard flow changes to always present an SDK selection step, prioritizing the detected SDK but allowing override (and falling back to manual selection when detection fails), and APIClients gains injectable Detector/Installer with production defaults wired in cmd/root.go. Output and template handling were tightened (avoid trailing @ when version is empty; align Android template ID to android-client-sdk; adjust Go template import guidance), with extensive new/updated tests for detection, install command selection/execution, and wizard transitions.

Reviewed by Cursor Bugbot for commit f60f9d1. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread internal/setup/detector.go Outdated
Copy link
Copy Markdown
Contributor

@ari-launchdarkly ari-launchdarkly left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really great work. I've got questions around whether we can make this a little more deterministic around some of the edges if we're leaning away from an agent to determine some of the dependencies. If it's not abundantly obvious for us, we can also punt on that

Comment on lines +82 to +108
if _, ok := allDeps["next"]; ok {
return &DetectResult{
Language: "JavaScript",
Framework: "Next.js",
PackageManager: pm,
SDKID: "node-server",
EntryPoint: filepath.Join(dir, firstExistingIn(dir, []string{
"src/index.ts", "src/index.js",
"pages/index.tsx", "pages/index.ts", "pages/index.js",
"index.js",
})),
}
}

if _, ok := allDeps["react"]; ok {
return &DetectResult{
Language: "JavaScript",
Framework: "React",
PackageManager: pm,
SDKID: "react-client-sdk",
EntryPoint: filepath.Join(dir, firstExistingIn(dir, []string{
"src/App.tsx", "src/App.jsx", "src/App.js",
"src/index.tsx", "src/index.jsx", "src/index.js",
"index.js",
})),
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's pretty tough to detect just plain JS with no framework, so maybe the SDK selector can just take care of that case.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. I think we can try and detect some of those based on known active frameworks. I think I put in the suggestion downstream

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's what we're doing now:

  1. Has next dep → node-server
  2. Has react-native dep → react-native
  3. Has react dep → react-client-sdk
  4. Has backbone/svelte/vue/angular/ember/preact dep → js-client-sdk
  5. Fallback: anything else with a package.json → node-server

Comment thread internal/setup/detector.go
Comment thread internal/setup/detector.go
Comment thread internal/setup/detector.go
Comment thread internal/setup/detector.go
Comment thread internal/setup/detector.go
Comment thread internal/setup/installer.go
Comment thread internal/setup/installer.go Outdated
Comment on lines +115 to +116
case "android", "android-client-sdk":
return nil, "com.launchdarkly:launchdarkly-android-client-sdk"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for this, the android SDK has a couple different ways of setting it depending on whether the user is using Kotlin or Groovy:

https://launchdarkly.com/docs/sdk/client-side/android#install-the-sdk

Is there a way for us to account for the package here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're accounting for Kotlin vs. Groovy, but we should surface to the user that they need to add the implementation 'com.launchdarkly:launchdarkly-android-client-sdk:5.+' to their build file. Unless we want to inject code here as well?

Comment on lines +113 to +114
case "java-server-sdk":
return nil, "com.launchdarkly:launchdarkly-java-server-sdk"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for this, the docs point to us having a way to do this in XML and Gradle:

https://launchdarkly.com/docs/sdk/server-side/java#install-the-sdk

Is that something we'd want to be deterministic about?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're deterministic about this detection, but we still are prompting the user to copy/paste a snippet for Gradle/Maven, since there's no shell command to import the LD SDK like with the other languages.

Comment thread internal/setup/detector.go
Comment thread internal/setup/detector.go Outdated
Comment thread internal/setup/detector.go Outdated
dakotasanchez and others added 6 commits May 13, 2026 12:08
Co-authored-by: ari-launchdarkly <asalem@launchdarkly.com>
Co-authored-by: ari-launchdarkly <asalem@launchdarkly.com>
Co-authored-by: ari-launchdarkly <asalem@launchdarkly.com>
Co-authored-by: ari-launchdarkly <asalem@launchdarkly.com>
@dakotasanchez dakotasanchez force-pushed the dsanchez/REL-13574/cli branch from 71c2501 to 2e055cf Compare May 13, 2026 19:09
Comment thread cmd/setup/wizard.go
Comment thread internal/setup/detector.go
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default mode and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f60f9d1. Configure here.

Comment thread cmd/setup/wizard.go
m.detectedEntryPoint = msg.result.EntryPoint
m.sdkList = m.buildSDKList(msg.result.SDKID)
m.step = stepSelectSDK
return m, nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Detected package manager discarded during SDK selection

High Severity

The detectDoneMsg handler stores EntryPoint in m.detectedEntryPoint but discards PackageManager. When the user confirms SDK selection in handleEnter, the new DetectResult is constructed without PackageManager, leaving it as an empty string. This causes InstallArgsresolveNodePM("") to always default to "npm", even when the detector correctly identified yarn, pnpm, or bun from lock files. The installer will run the wrong package manager command (e.g. npm install in a pnpm project).

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f60f9d1. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants