Skip to content

[REL-14909] Revive CLI Quickstart Wizard — integrate #708 + #709#744

Closed
ffantl-ld wants to merge 33 commits into
mainfrom
ffantl/REL-14909/revive-cli-wizard
Closed

[REL-14909] Revive CLI Quickstart Wizard — integrate #708 + #709#744
ffantl-ld wants to merge 33 commits into
mainfrom
ffantl/REL-14909/revive-cli-wizard

Conversation

@ffantl-ld

@ffantl-ld ffantl-ld commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Integration branch for REL-14909 — reviving the deterministic CLI Quickstart Wizard (ldcli setup). Stacks the two moonshot PRs on top of current main:

Rebased #709's branch (dsanchez/REL-13574/cli, itself based on #708's ari-launchdarkly/REL-0000-cli) onto current main — 15 commits, clean rebase, go build ./... passes.

Status — DRAFT, needs validation

Opened as a draft to get an end-to-end test pass. Known open items:

  • [REL-13574] Implement SDK detection and installation #709 carried CHANGES_REQUESTED — review not yet re-resolved on this branch.
  • End-to-end manual validation (detect on real React/Go/Python/Java projects; install shells out; init injects; verify polls sdk-active).
  • go test ./... on the rebased tree.

Intent

Deterministic, bounded, inspectable CLI install flow (Plan item #3 / H2 of the Agentic Onboarding plan) — same automation as the AI-install path, offered to the ~49% who prefer manual install and the segment an open-ended agent can't serve. The hidden JSON subcommands mean one engine backs both this wizard and external AI agents.

Ref: REL-14909 · supersedes moonshot epic REL-13574.

ari-launchdarkly and others added 30 commits July 15, 2026 23:48
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>
Address the six findings from the independent test pass on the revived
CLI onboarding wizard:

- init: no longer writes non-compiling code for scoped/framework SDKs.
  Only script languages (node/python/ruby) are appended in place; Go,
  Java, C#, Swift, Android, React, and React Native now return the
  rendered snippet + docs URL as guidance with success=false instead of
  corrupting the entry file and reporting success. Drops the Go AST
  import-merge path (dead once Go is guidance-only).
- react: covered by the guidance path above (asyncWithLDProvider must be
  wired into the component tree, not appended).
- detect: add a Ruby detector (Gemfile / Gemfile.lock / config.ru / *.gemspec).
- install: add --dry-run to print the command without executing; unknown
  SDK IDs now error instead of silently returning success=false, while
  genuine manual-install SDKs (Java/Android/Swift) still return success=false.
- gofmt: format previously-misindented files.
- detect: clarify with a comment that Next.js intentionally maps to the
  Node server SDK (server-side eval for SSR / API routes).

Tests updated to assert guidance behavior for scoped/framework SDKs and
to cover Ruby detection, dry-run, and the unknown-SDK error.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Running `ldcli setup` without an access token launched the TUI and then
died on the first API call with a raw 401 JSON dump ("Invalid account ID
header") and no guidance — the exact first-run case a new user hits.

Check for an access token before launching the wizard and return a clear
message pointing at `ldcli login` (or --access-token / LD_ACCESS_TOKEN)
instead. Adds a test covering the no-token path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reword the pre-flight auth message to a conversational nudge that points
at `ldcli login` as the next step.

On the SDK selection screen, when detection succeeded, replace the
generic "Select your SDK:" title with "We've detected <SDK> — press
Enter to use it, or choose a different SDK below:" so it's clear the
top entry is the auto-detected one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The quickstart-transition notice printed flush against command output,
reading as part of it. Render it faint and follow it with a blank line
so it's visually distinct from the wizard's output and messages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The detected SDK now appears in its own "We identified this as your SDK"
panel above a separate list of the remaining SDKs. Arrow keys move focus
between the two areas (the focused one is highlighted); Enter on the
panel accepts the detected SDK, Enter in the list picks another. When
detection fails, only the list is shown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The success, manual-setup, and timed-out screens are terminal states but
gave no exit hint. Append a faint "Press q to quit." to each.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The detected-SDK panel gave no cursor, so the list's highlight on the
first other SDK read as the active choice. Add a purple pointer to the
detected SDK while its panel is focused.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Give the detected-SDK panel and the other-SDKs list one shared width so
they line up, and render the list's selected row as a normal row while
focus is on the panel — so only the focused area shows an active
selection instead of both the panel and the first list row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Before running the package manager, check whether the SDK's package is
already present in the project manifest (package.json, go.mod,
requirements.txt, Gemfile, *.csproj). If so, return AlreadyInstalled and
skip the install instead of re-running it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
After the SDK is chosen, show a plan of what setup will do — install (or
"already installed, will skip"), create a flag, add init code (or show a
snippet), and verify — and wait for Enter before taking any action.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the "Hey —" opener and put the call to action on its own line
after a blank line, so the message reads cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the per-screen color literals (6/205/240) scattered across the
wizard with a small shared palette and style set in styles.go, aligned
with ldcli's quickstart TUI (selected 170, border 62). Screens now share
titleStyle/headerStyle/selectedStyle/mutedStyle and a box() helper
instead of re-declaring them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The list rendered its title with the default colored title bar while the
detected-SDK panel header was plain bold, so the two headings looked
inconsistent. Style the list title with the same header style.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Left arrow steps back through the flow (plan -> SDK -> environment ->
project) so the SDK, environment, or project can be changed. Esc quits.
Both are suppressed while a list filter is active so they don't hijack
filter editing. Screens show the "← back · esc quit" hints.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Detection ran when the SDK screen was reached and re-ran whenever the
user navigated back and forward. Kick it off in Init instead, cache the
result, and build the SDK screen from the cache — so moving through the
flow is instant and detection never runs twice.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ffantl-ld and others added 3 commits July 21, 2026 10:35
The SDK screen showed two key hints — the list's built-in help footer and
a separate line below. Turn off the list's built-in help and render one
hint inside the other-SDKs box (↑/↓ move · enter select · ← back · esc quit).
Also accept vim `h` as a back key alongside the left arrow (j/k already
handle down/up).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rapping

- A failed auto-install (e.g. Ruby gem perms, no network) no longer
  dead-ends the wizard: it continues and the final screen shows the
  command to run by hand, and skips the live verify.
- SDKs with no automated install (Java/Android/Swift) are marked
  "(manual install)" in the SDK list and detected panel.
- The SDK screen links to the full SDK catalog so an unlisted language
  doesn't read as unsupported.
- Boxes clamp to the terminal width and prose reflows, so narrow
  terminals don't overflow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Manual-setup and failed-install screens printed the copy-me snippet and
install command as plain text, indistinguishable from surrounding prose.
Render them through a code style (left gutter bar, distinct foreground)
so the user can tell what to copy versus what to read.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ffantl-ld

Copy link
Copy Markdown
Contributor Author

Superseded by a progressive split of this work. Landing as small, reviewable PRs onto the setup-ld integration branch instead of one large merge:

setup-ld will merge to main once the stack is complete.

@ffantl-ld ffantl-ld closed this Jul 22, 2026

@ffantl-ld ffantl-ld left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Review of the revived setup wizard. Structure and test coverage are solid. Comments below, highest-value first — the root.go one touches shared behavior outside setup/ and is worth verifying before un-drafting.

via LD Research 🤖

Comment thread cmd/root.go
// 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{

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Shared behavior change for 6 commands, not just setup. The old code set DisableFlagParsing = true for completion/config/help/login/signup/whoami; this replaces that with only clearing the --access-token required annotation. Cleaner intent, but not equivalent: any of those commands that relied on flag parsing being disabled — or had other required flags previously skipped — now behave differently. completion in particular passes args through.

Please run go test ./cmd/... on the rebased tree and smoke ldcli completion bash, ldcli config, ldcli whoami before un-drafting. This is the only change reaching outside setup/.

via LD Research 🤖

Comment thread cmd/setup/wizard.go

// Commands that perform async work

func (m wizardModel) fetchProjects() tea.Cmd {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No pagination. /api/v2/projects defaults to 20 items. A user with >20 projects silently can't select the rest. Same for fetchEnvironments. Add ?limit= + follow _links.next, or at minimum request a high limit. Filing a REL follow-up unless you want it in this PR.

via LD Research 🤖

@@ -0,0 +1,15 @@
const LaunchDarkly = require('@launchdarkly/node-server-sdk');

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Assumes CommonJS. node-server is append-safe, so this require(...) is written directly into the entry file. A project with "type": "module" in package.json throws at runtime. The detector doesn't read module type. Either detect ESM and emit import, or degrade node to guidance-only for ESM projects.

via LD Research 🤖

Comment thread cmd/setup/wizard.go
Message string `json:"message"`
}

func parseJSONError(err error) (*jsonError, error) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fragile coupling. parseJSONError assumes err.Error() is raw JSON. If flagsClient.Create ever wraps the error (e.g. fmt.Errorf), conflict detection breaks and the wizard hard-errors instead of reusing the existing my-new-flag. Prefer a typed/sentinel error from the flags client.

via LD Research 🤖

Comment thread cmd/setup/wizard.go
if err != nil {
// Don't dead-end on a failed auto-install (e.g. Ruby gem perms, no
// network): continue and surface the command to run by hand.
args, _ := setup.InstallArgs(m.detectResult.SDKID, m.detectResult.PackageManager)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Install error is swallowed. The real err (network, gem perms, etc.) is discarded here; the manual-install screen shows only the command. The user can't tell why the auto-install failed. Carry the error message onto the failure screen.

via LD Research 🤖

Comment thread cmd/setup/wizard.go
return titleStyle.Render("Setup complete!") + "\n\n" +
fmt.Sprintf("Your %s SDK is connected to LaunchDarkly.\n", m.detectResult.SDKID) +
fmt.Sprintf("Flag %q is ready to use.\n\n", m.flagKey) +
fmt.Sprintf("You can now toggle your flag at https://app.launchdarkly.com/projects/%s/flags/%s/targeting?env=%s\n", m.selectedProject, m.flagKey, m.selectedEnv) +

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hardcoded app host. https://app.launchdarkly.com ignores baseURI — wrong link on federal / custom instances. Derive from the configured base URI.

via LD Research 🤖

pkg = "@launchdarkly/js-client-sdk"
return nodeInstallCmd(resolveNodePM(packageManager), pkg), pkg
case "python-server-sdk":
pm := packageManager

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Minor / intentional-per-test: the Python path uses --package-manager verbatim as args[0] while the Node path normalizes it. Not shell-injectable (exec, not sh) and it's the user's own machine + flag, so low risk — just flagging the inconsistency.

via LD Research 🤖

Language: "Java",
PackageManager: "gradle",
SDKID: "android-client-sdk",
EntryPoint: filepath.Join(dir, "app/src/main/java/MainActivity.java"),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Cosmetic: app/src/main/java/MainActivity.java is a guessed path — Android sources normally sit under package dirs. Android is guidance-only (not injected), so this only affects the displayed hint.

via LD Research 🤖

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.

3 participants