Skip to content

feat(setup): Add new wizard setup command to the CLI - #776

Open
ffantl-ld wants to merge 13 commits into
mainfrom
setup-ld
Open

feat(setup): Add new wizard setup command to the CLI#776
ffantl-ld wants to merge 13 commits into
mainfrom
setup-ld

Conversation

@ffantl-ld

@ffantl-ld ffantl-ld commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

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

Related issues

Provide links to any issues in this repository or elsewhere relating to this pull request.

Describe the solution you've provided

Provide a clear and concise description of what you expect to happen.

Describe alternatives you've considered

Provide a clear and concise description of any alternative solutions or features you've considered.

Additional context

Add any other context about the pull request here.


Note

Overview
Replaces the old setup entry with a guided Bubble Tea wizard that detects language/framework, lets the user pick project/env/SDK (and package manager when ambiguous), previews a plan, then installs the SDK, creates my-new-flag, injects init code, and verifies the SDK is active. The previous wizard is kept as hidden, deprecated ldcli quickstart.

Hidden setup detect|install|init subcommands expose the same steps for scripts (JSON/plaintext). Detection prefers backend manifests over a root package.json, distinguishes found vs guessed entry points, and refuses to guess an ambiguous package manager. Install/init fall back to copyable commands/snippets; c copies via OS clipboard or OSC 52 (SSH skips the remote clipboard).

Also paginates project/env lists, and creates the demo flag with client-side and mobile availability so browser/mobile SDKs can evaluate it. CI now pins Go from go.mod so golangci-lint stays compatible.

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

@ffantl-ld
ffantl-ld requested a review from a team as a code owner August 6, 2026 17:52
Comment thread internal/setup/installer.go
Comment thread internal/setup/service.go
ffantl-ld and others added 5 commits August 7, 2026 11:27
* chore(setup): add SDK/project detection library

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

* fix(setup): report whether the detected entry point exists

DetectResult carries EntryPointExists so callers can tell an entry file the
detector found from one it merely suggests, and never write initialization code
into a path the project does not load.

PackageManager names the tool that manages the project's dependencies: bundle
rather than gem when a Gemfile is present, and poetry, uv or pipenv rather than
always pip.

Locate MainActivity and Main under their real package directory rather than
assuming an unqualified class name, derive the Android source root from
whichever manifest matched, find the Swift entry point where SwiftPM and Xcode
nest it, look for src/main.tsx where Vite mounts a React app, and recognise
bun.lockb.

Rename the Android SDK ID to android for consistency with the other IDs.

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

* fix(setup): keep the Next.js SDK key out of the browser bundle

Next.js detection targeted whichever page module happened to exist, and
node-server is append-safe, so setup wrote server SDK init — including the SDK
key — into app/page.tsx or pages/index.tsx. A page module may carry 'use client'
or be imported by something that does, which bundles it for the browser, and
nothing in the detector can tell which. Only instrumentation.ts, Next's
server-startup hook, is guaranteed to stay server-side, so suggest creating it
rather than picking a page.

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

* docs(setup): cite the sources for the entry-point candidates

Each candidate list encodes a claim about where a toolchain puts its entry file.
Link the documentation that claim rests on so it can be rechecked when the
frameworks move.

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

* fix(setup): detect backend manifests before package.json

A root package.json is often only build tooling — Rails with jsbundling, Django
with Tailwind, a Go binary published to npm — so preferring Node whenever one
parsed meant those projects were handed the Node SDK. This repo hit it too.

Confine the Sources/ search to single-target Swift packages. Across several
targets there is no way to tell an executable's entry file from a library's, so
an arbitrary hit was reported as found.

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

* test(setup): assert the whole result per project shape

The existing tests check one or two fields each, so a field detection stops
populating passes as long as the SDK id stays right. Compare the full
DetectResult across the project layouts real toolchains produce.

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

* fix(setup): find the src/main entry a Node app bootstraps from

NestJS and similar apps start from src/main.ts, which the candidate list
skipped, so detection suggested a nonexistent index.js. node-server appends to
the entry file, so setup created that index.js and left the real entry point
without the SDK.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(setup): add SDK/project detection library

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

* fix(setup): report whether the detected entry point exists

DetectResult carries EntryPointExists so callers can tell an entry file the
detector found from one it merely suggests, and never write initialization code
into a path the project does not load.

PackageManager names the tool that manages the project's dependencies: bundle
rather than gem when a Gemfile is present, and poetry, uv or pipenv rather than
always pip.

Locate MainActivity and Main under their real package directory rather than
assuming an unqualified class name, derive the Android source root from
whichever manifest matched, find the Swift entry point where SwiftPM and Xcode
nest it, look for src/main.tsx where Vite mounts a React app, and recognise
bun.lockb.

Rename the Android SDK ID to android for consistency with the other IDs.

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

* fix(setup): keep the Next.js SDK key out of the browser bundle

Next.js detection targeted whichever page module happened to exist, and
node-server is append-safe, so setup wrote server SDK init — including the SDK
key — into app/page.tsx or pages/index.tsx. A page module may carry 'use client'
or be imported by something that does, which bundles it for the browser, and
nothing in the detector can tell which. Only instrumentation.ts, Next's
server-startup hook, is guaranteed to stay server-side, so suggest creating it
rather than picking a page.

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

* docs(setup): cite the sources for the entry-point candidates

Each candidate list encodes a claim about where a toolchain puts its entry file.
Link the documentation that claim rests on so it can be rechecked when the
frameworks move.

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

* fix(setup): detect backend manifests before package.json

A root package.json is often only build tooling — Rails with jsbundling, Django
with Tailwind, a Go binary published to npm — so preferring Node whenever one
parsed meant those projects were handed the Node SDK. This repo hit it too.

Confine the Sources/ search to single-target Swift packages. Across several
targets there is no way to tell an executable's entry file from a library's, so
an arbitrary hit was reported as found.

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

* test(setup): assert the whole result per project shape

The existing tests check one or two fields each, so a field detection stops
populating passes as long as the SDK id stays right. Compare the full
DetectResult across the project layouts real toolchains produce.

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

* fix(setup): find the src/main entry a Node app bootstraps from

NestJS and similar apps start from src/main.ts, which the candidate list
skipped, so detection suggested a nonexistent index.js. node-server appends to
the entry file, so setup created that index.js and left the real entry point
without the SDK.

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

* chore(setup): add SDK installer library

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

* fix(setup): install with the project's own package manager

`gem install` left the Gemfile untouched, so the SDK stayed unavailable under
bundler and IsInstalled kept returning false. Use `bundle add` when the project
is Bundler-managed, and poetry, uv or pipenv when one of those manages the
Python dependencies.

Unrecognised package managers fall back to pip rather than being run as a
command, since the value reaches InstallArgs from the detector.

InstallArgs added launchdarkly-react-native-client-sdk, which npm marks
deprecated in favour of @launchdarkly/react-native-client-sdk. The unscoped
launchdarkly-js-client-sdk is the v3 package whose initialize API the init
template uses; the scoped one is v4 and exposes createClient.

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

* fix(setup): match whole packages and target the right .NET project

IsInstalled tested for its package name as a substring, so
@launchdarkly/node-server-sdk-redis, launchdarkly-server-sdk-otel, and
LaunchDarkly.ServerSdk.Telemetry each counted as the SDK itself and the real
install was skipped. Require a non-name character on both sides, which every
manifest format supplies.

Detection accepts a solution with no project file beside it, but install ran a
bare `dotnet add package`, which needs the working directory to hold exactly
one project. Resolve the project the solution refers to and pass --project;
with none or several, stop and say so rather than adding the SDK to an
arbitrary assembly.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(setup): add SDK/project detection library

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

* fix(setup): report whether the detected entry point exists

DetectResult carries EntryPointExists so callers can tell an entry file the
detector found from one it merely suggests, and never write initialization code
into a path the project does not load.

PackageManager names the tool that manages the project's dependencies: bundle
rather than gem when a Gemfile is present, and poetry, uv or pipenv rather than
always pip.

Locate MainActivity and Main under their real package directory rather than
assuming an unqualified class name, derive the Android source root from
whichever manifest matched, find the Swift entry point where SwiftPM and Xcode
nest it, look for src/main.tsx where Vite mounts a React app, and recognise
bun.lockb.

Rename the Android SDK ID to android for consistency with the other IDs.

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

* fix(setup): keep the Next.js SDK key out of the browser bundle

Next.js detection targeted whichever page module happened to exist, and
node-server is append-safe, so setup wrote server SDK init — including the SDK
key — into app/page.tsx or pages/index.tsx. A page module may carry 'use client'
or be imported by something that does, which bundles it for the browser, and
nothing in the detector can tell which. Only instrumentation.ts, Next's
server-startup hook, is guaranteed to stay server-side, so suggest creating it
rather than picking a page.

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

* docs(setup): cite the sources for the entry-point candidates

Each candidate list encodes a claim about where a toolchain puts its entry file.
Link the documentation that claim rests on so it can be rechecked when the
frameworks move.

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

* fix(setup): detect backend manifests before package.json

A root package.json is often only build tooling — Rails with jsbundling, Django
with Tailwind, a Go binary published to npm — so preferring Node whenever one
parsed meant those projects were handed the Node SDK. This repo hit it too.

Confine the Sources/ search to single-target Swift packages. Across several
targets there is no way to tell an executable's entry file from a library's, so
an arbitrary hit was reported as found.

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

* test(setup): assert the whole result per project shape

The existing tests check one or two fields each, so a field detection stops
populating passes as long as the SDK id stays right. Compare the full
DetectResult across the project layouts real toolchains produce.

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

* fix(setup): find the src/main entry a Node app bootstraps from

NestJS and similar apps start from src/main.ts, which the candidate list
skipped, so detection suggested a nonexistent index.js. node-server appends to
the entry file, so setup created that index.js and left the real entry point
without the SDK.

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

* chore(setup): add SDK installer library

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

* fix(setup): install with the project's own package manager

`gem install` left the Gemfile untouched, so the SDK stayed unavailable under
bundler and IsInstalled kept returning false. Use `bundle add` when the project
is Bundler-managed, and poetry, uv or pipenv when one of those manages the
Python dependencies.

Unrecognised package managers fall back to pip rather than being run as a
command, since the value reaches InstallArgs from the detector.

InstallArgs added launchdarkly-react-native-client-sdk, which npm marks
deprecated in favour of @launchdarkly/react-native-client-sdk. The unscoped
launchdarkly-js-client-sdk is the v3 package whose initialize API the init
template uses; the scoped one is v4 and exposes createClient.

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

* fix(setup): match whole packages and target the right .NET project

IsInstalled tested for its package name as a substring, so
@launchdarkly/node-server-sdk-redis, launchdarkly-server-sdk-otel, and
LaunchDarkly.ServerSdk.Telemetry each counted as the SDK itself and the real
install was skipped. Require a non-name character on both sides, which every
manifest format supplies.

Detection accepts a solution with no project file beside it, but install ran a
bare `dotnet add package`, which needs the working directory to hold exactly
one project. Resolve the project the solution refers to and pass --project;
with none or several, stop and say so rather than adding the SDK to an
arbitrary assembly.

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

* chore(setup): add SDK init/injection library and templates

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

* fix(setup): correct the React Native init snippet

react-native.tmpl bound the default export of
@launchdarkly/react-native-client-sdk, which has no default export, so the
snippet could not compile. The client is the named export ReactNativeLDClient.

Register the android template under the SDK ID the detector reports, keeping
android-client-sdk as an alias.

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

* fix(setup): pass the required auto-env argument in mobile snippets

The Swift and Android snippets did not compile. LDConfig's only public
initializer takes autoEnvAttributes, and LDConfig.Builder's only constructor
takes AutoEnvAttributes, so neither config could be built as written. These
SDKs return a snippet for the user to paste rather than writing a file, so the
snippet is the entire deliverable.

AutoEnvAttributes is nested in LDConfig.Builder, which the package wildcard
import does not cover, so import it explicitly.

Add DefaultEntryPoint, naming the file to create for the SDKs that write one
when detection found no entry point for them.

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

* fix(setup): build the Swift context without try

LDContextBuilder.build returns a Result, and the snippet unwrapped it with
`try ...get()`. try only compiles inside a throwing function, and the places
this snippet gets pasted — application(_:didFinishLaunchingWithOptions:) and
similar startup hooks — do not throw. Match on the Result instead.

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

* fix(setup): keep the shebang first when injecting imports

InjectIntoFile prepended the import section at byte 0, which displaced a leading
shebang and an encoding cookie. Django's manage.py is one of the Python entry
points detection targets, so `./manage.py` stopped being executable after setup
wrote to it, and Python stopped honoring a coding declaration pushed past line 2.

Peel the prologue off first and insert imports after it.

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

* fix(setup): inject node imports in the entry file's module syntax

The node-server template loaded the SDK with require, but detection targets
TypeScript and ESM entry points such as Next.js instrumentation.ts and NestJS
src/main.ts, where require is not defined at runtime. Setup wrote that code and
reported success, so the failure only showed up when the app started.

Pick the CommonJS or ESM template from the entry extension, falling back to the
nearest package.json "type" for a plain .js file.

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

* fix(setup): keep leading directives above injected imports

Injecting imports after only the shebang and encoding cookie broke the other
constructs a file has to open with. A Python __future__ import below the injected
imports is a SyntaxError, a module docstring pushed down is demoted to a plain
expression, and Ruby magic comments and a CommonJS 'use strict' are silently
ignored once code precedes them. All of it was written with Success: true.

Peel the shebang, the leading comment block, and the language's own leading
constructs, then insert imports after them.

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

* fix(setup): treat block comments as part of the leading header

A /* */ license or JSDoc header stopped the prologue scan, so imports landed
above it and above the 'use strict' that followed, silently dropping the file out
of strict mode. A directive with a trailing same-line comment was missed for the
same reason.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(setup): add setup flag verifier

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

* fix(setup): narrow the verify check to the configured SDK

sdk-active answers for any SDK that initialized in the environment in the past
seven days, so verification passed on unrelated traffic and a project already
using LaunchDarkly reported success without the new SDK ever connecting.

Filter on sdk_name, mapping the setup SDK id to the name the SDK reports
itself as. An unknown id sends no filter rather than one that can never match,
and VerifyResult records which name was used so an unnarrowed check cannot
present itself as narrowed.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: add guided setup command

ldcli setup walks a project through installing a LaunchDarkly SDK: detect the
language and package manager, pick a project and environment, install the SDK
with the project's own tool, create a flag, write or show initialization code,
then poll until the SDK connects.

Orchestration lives in internal/setup.Service so the wizard UI and the
detect/install/init subcommands share one path. The wizard is split into model,
update, view, and commands rather than one file.

Environments gains List so the wizard can offer a choice of environments.

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

* feat(setup): copy wizard code blocks with c (#771)

* REL-15243: add a copy key for wizard code blocks

Code blocks are drawn with a left gutter bar, so selecting one by hand copies the
gutter characters and the padding lipgloss squares the block off with. The wizard
also owns the alternate screen, so the snippet is not in scrollback once it exits.

Pressing c writes the raw content to the system clipboard with OSC 52, preferring
the snippet over the install command when a screen shows both.

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

* REL-15243: copy through the OS clipboard before the terminal

OSC 52 alone left the key unreliable: terminals are not required to implement it,
Apple Terminal does not, and support cannot be queried, so the confirmation claimed
a copy that may never have happened. The OS clipboard works in any terminal and
returns an error, so try it first and keep OSC 52 for when it fails — which is the
SSH case, where the OS clipboard belongs to the wrong machine.

Word the two outcomes apart, since only the first can be confirmed.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ffantl-ld

Copy link
Copy Markdown
Contributor Author

I'm following up on a couple of bugs found on this branch. Will post the ticket and PRs as soon as they're ready.

Arrow keys arrive as the escape sequence ESC [ A. bubbletea v1.3.6 reports a
bare esc whenever a read returns only the first byte, so scrolling a list could
drop the user out of the wizard at random.

Quit is now q or ctrl+c only, and the on-screen hints match. Leaving esc unbound
also returns it to the list for clearing an active filter, which fixes the
stranded filter state that silently disabled q on the SDK screen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ffantl-ld and others added 2 commits August 19, 2026 10:05
bubbles binds a list's quit key to both q and esc and returns tea.Quit when either
matches, so dropping our own esc binding was not enough: the key fell through to
whichever list was on screen and ended the session from there. Each list's quit
key is now q alone. esc still clears an active filter, which the list matches
ahead of quitting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread internal/setup/service.go
Availability was set only when an SDK's init template interpolates the client-side
ID, so React Native, Android and Swift — which take the mobile key — created flags
with mobile availability off. On a project whose defaults are off, those SDKs
evaluate the fallback forever while setup reports success.

Both settings are now always on: the SDK a project starts with does not say which
credential it will end up using. A flag that already exists is still left exactly
as it is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ffantl-ld
ffantl-ld marked this pull request as draft August 19, 2026 18:16
@ffantl-ld
ffantl-ld marked this pull request as ready for review August 20, 2026 15:48
ffantl-ld and others added 2 commits August 20, 2026 13:17
The workflow asked for whatever Go is newest, so the 1.27.0 release broke every
pull request at once: golangci-lint reads type data written by the compiler, and
the pinned linter cannot read a format newer than the Go it was released for. The
dependency scan already reads go.mod for this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(setup): resolve pip and python from PATH before installing

Recent macOS and Homebrew installs ship python3/pip3 with no bare python or
pip, so the hardcoded `pip install` failed outright on a common developer box.
Probe pip3 then pip, falling back to `<interpreter> -m pip` for interpreters
installed without a pip shim.

Every package manager is now checked for existence before being run, so a
missing tool reports what to install instead of an exec "not found" error.

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

* fix(setup): verify pip is importable before using the -m pip form

Debian and Ubuntu package pip separately from the interpreter, so a present
python3 does not mean `python3 -m pip` can run. The pre-flight check only looked
at the executable, so those boxes got "No module named pip" instead of guidance.

Stub PATH in the two install tests that reached for a real npm, which the
pre-flight check made environment-dependent.

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

* fix(setup): never substitute a Python interpreter for a missing pip

Using `python3 -m pip`, or bootstrapping pip with ensurepip, would install
tooling onto the user's machine. Setup only ever uses a pip that is already
there; when none is found it warns and runs nothing.

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

* fix(setup): install Python packages into the project virtualenv

pip refuses to write into an OS-managed Python, which Homebrew and most current
distributions now mark, so a project with no active virtualenv could not complete
setup at all. A virtualenv's pip is used when the project or the environment has
one, and the refusal is explained rather than passed through as pip's raw error.

InstallArgs takes the project directory so the previewed command is the one that
actually runs.

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

* test(setup): neutralise the ambient virtualenv for the package

pipInstallCmd prefers VIRTUAL_ENV over anything on PATH, so running the suite
inside an activated environment resolved install commands to that environment's
pip and failed assertions about pip and pip3. Tests that want an active
virtualenv opt in with stubVirtualEnv.

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

* fix(setup): resolve the virtualenv pip to an absolute path

The install runs with its working directory set to the project, and a relative
executable path is resolved after that change, so a relative project directory was
applied twice: "app/.venv/bin/pip" run in "app" was looked for at
"app/app/.venv/bin/pip" and the install failed even though the venv was found.

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

* fix(setup): report a virtualenv that has no pip

`uv venv` creates a virtualenv without pip, which was read as no virtualenv at all:
setup then reached for a pip on PATH, installing outside the project the user set
up — or being refused by PEP 668 and advising them to create the virtualenv already
sitting there. That case is now named, with how to install into it.

A PEP 668 refusal no longer carries the command that refused. The done screen
offers a non-empty command as "install it yourself with", which contradicted the
reason telling them not to run it.

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

* fix(setup): name the project virtualenv in the previewed pip command

The pip-less virtualenv guard sat only in Install, so the plan screen, --dry-run
and the picker still previewed a pip from PATH that Install would refuse to run —
and a command shown there is one a reader may run by hand, installing outside the
project or hitting a raw PEP 668 error.

The command now names the virtualenv's own pip whenever the project has one,
present or not, so every surface and the runner agree. Install still explains why
an unseeded virtualenv cannot be installed into.

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

* fix(setup): use the platform's virtualenv layout and flag an unrecorded dependency

A virtualenv with no pip was always named bin/pip, so on Windows the plan and the
previewed command pointed at a layout that environment never uses. The platform's
own layout is named first, and both are still considered.

A bare pip install also leaves the project's manifest untouched, so a fresh checkout
and CI miss the SDK. poetry, uv and pipenv record it themselves and Ruby has
`bundle add`; pip has no equivalent, and editing someone's manifest unasked is not
something setup does, so a successful install now says what is still missing.

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

* fix(setup): match the SDK by whole name when checking the manifest

A substring match read a related pin such as launchdarkly-server-sdk-otel as the SDK
itself, so the warning stayed quiet while the project still lacked the dependency.
The whole-name matcher the install-skipping check already uses does the job.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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 effort 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 fbbbd11. Configure here.

Comment thread cmd/setup/view.go Outdated
…#782)

* fix(setup): re-derive entry point and package manager on SDK override

Choosing an SDK by hand replaced the detected entry point with that SDK's bare
default in the working directory, so a project whose entry file was src/index.js
got a second index.js created beside it. The package manager was left describing
the language detection guessed first, so a Ruby install never ran bundle add.

Both are now re-derived for the chosen SDK from one shared table of entry-point
candidates, which detection also reads. Snippet-only SDKs have no entry point, so
the final screen no longer names an empty path.

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

* feat(setup): report package-manager confidence and stop guessing silently (#783)

* feat(setup): report package-manager confidence and stop guessing silently

Detection always returned a manager, so a project that never said which one it
used got pip or npm presented as fact. Signals are now split into what the
project declares and what it merely implies, and a verdict is definite only when
the project names exactly one manager.

Recognise the corepack packageManager field, poetry.lock, Pipfile.lock and
[tool.pdm], which were previously read as pip or npm. [tool.hatch] is recorded as
a signal we cannot act on, since hatch has no dependency-add command.

`setup install` now reads the project when --package-manager is omitted, instead
of defaulting to pip or npm whatever the lockfiles say, and fails with the
candidate list when the project is ambiguous rather than picking for the user.

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

* fix(setup): match nested tool tables and let a committed manager win

Real pyproject files rarely carry a bare [tool.x] header, so matching only that
left the poetry, uv, pdm and hatch signals near-dead: a hatch project configures
[tool.hatch.build], not [tool.hatch]. Nested tables now count, with the trailing
delimiter required so [tool.uv] does not match [tool.uvicorn].

A manager the project committed to now settles the verdict even when an
unactionable tool is also configured. hatchling is a common build backend for uv
and poetry projects, and uv can add the dependency whoever builds the wheel;
previously those projects were marked ambiguous and install refused to run.

Recognise pdm.lock, so a PDM project that commits only its lockfile is still
identified as one.

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

* fix(setup): require a version in the packageManager field

pnpm refuses to run at all when package.json names it without a version — "No
version specified for pnpm in packageManager" — so treating a versionless field
as the project's declared manager routed the user into a command that cannot
work. Such a field is no longer a declaration: the lockfiles decide, or the user
is asked.

When a manager still refuses for that reason, say so. The manifest is malformed
rather than the command wrong, and repairing someone's manifest is not ours to do.

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

* fix(setup): require one exact version in the packageManager field

Corepack accepts only an exact version, so a range is refused outright —
"Invalid package manager specification in package.json (pnpm@^11.13.0); expected
a semver version" — as is a missing version. Treating either as the project's
declared manager routed the user into a command that cannot run. Only an exact
MAJOR.MINOR.PATCH, optionally with prerelease or build metadata, now counts;
anything else leaves the lockfiles to decide or the user to be asked.

When a manager refuses for that reason, say which part is wrong. The manifest is
malformed rather than the command, and repairing someone's manifest is not ours
to do.

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

* fix(setup): read pyproject.toml as TOML to find configured tools

Matching [tool.<name>] as text counted comments and strings, so a uv project whose
comment mentioned the tool it migrated away from was marked ambiguous and install
refused to run. Reading the declared tables instead means only a declaration counts,
and nested tables need no special case: TOML creates the parent implicitly, so
[tool.hatch.build] alone still declares hatch.

A file we cannot parse declares nothing, which leaves the project ambiguous and the
user asked — the honest answer when we cannot read what manages it.

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

* refactor(setup): read pyproject.toml once per detection

Detection parsed the file twice for a Python project: once for the detector's own
package manager and again for the confidence verdict. The verdict is now the single
source for the languages it models, and the detector leaves the field to it. A
language it does not model, such as Java's maven versus gradle, keeps the answer its
detector gives.

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

* fix(setup): stop misreading other ecosystems' install errors

The packageManager guidance matched a missing or non-semver version anywhere in an
install failure, so a gem, a Python package or a Go module reporting either phrase
had its real error replaced by advice about a file it does not have. The output has
to name package.json, which both corepack refusals do.

Say what was actually found when a project is set up for more than one manager: a
Pipfile and a [tool.*] table count as commitments too, so naming lockfiles sent the
reader looking for files that are not there.

Note on stderr when no --package-manager was given, since that used to fall through
to npm or pip and now reads the project. Callers parsing output are unaffected.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(setup): ask which package manager to use when the project is ambiguous (#784)

* feat(setup): report package-manager confidence and stop guessing silently

Detection always returned a manager, so a project that never said which one it
used got pip or npm presented as fact. Signals are now split into what the
project declares and what it merely implies, and a verdict is definite only when
the project names exactly one manager.

Recognise the corepack packageManager field, poetry.lock, Pipfile.lock and
[tool.pdm], which were previously read as pip or npm. [tool.hatch] is recorded as
a signal we cannot act on, since hatch has no dependency-add command.

`setup install` now reads the project when --package-manager is omitted, instead
of defaulting to pip or npm whatever the lockfiles say, and fails with the
candidate list when the project is ambiguous rather than picking for the user.

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

* fix(setup): match nested tool tables and let a committed manager win

Real pyproject files rarely carry a bare [tool.x] header, so matching only that
left the poetry, uv, pdm and hatch signals near-dead: a hatch project configures
[tool.hatch.build], not [tool.hatch]. Nested tables now count, with the trailing
delimiter required so [tool.uv] does not match [tool.uvicorn].

A manager the project committed to now settles the verdict even when an
unactionable tool is also configured. hatchling is a common build backend for uv
and poetry projects, and uv can add the dependency whoever builds the wheel;
previously those projects were marked ambiguous and install refused to run.

Recognise pdm.lock, so a PDM project that commits only its lockfile is still
identified as one.

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

* fix(setup): require a version in the packageManager field

pnpm refuses to run at all when package.json names it without a version — "No
version specified for pnpm in packageManager" — so treating a versionless field
as the project's declared manager routed the user into a command that cannot
work. Such a field is no longer a declaration: the lockfiles decide, or the user
is asked.

When a manager still refuses for that reason, say so. The manifest is malformed
rather than the command wrong, and repairing someone's manifest is not ours to do.

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

* fix(setup): require one exact version in the packageManager field

Corepack accepts only an exact version, so a range is refused outright —
"Invalid package manager specification in package.json (pnpm@^11.13.0); expected
a semver version" — as is a missing version. Treating either as the project's
declared manager routed the user into a command that cannot run. Only an exact
MAJOR.MINOR.PATCH, optionally with prerelease or build metadata, now counts;
anything else leaves the lockfiles to decide or the user to be asked.

When a manager refuses for that reason, say which part is wrong. The manifest is
malformed rather than the command, and repairing someone's manifest is not ours
to do.

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

* fix(setup): read pyproject.toml as TOML to find configured tools

Matching [tool.<name>] as text counted comments and strings, so a uv project whose
comment mentioned the tool it migrated away from was marked ambiguous and install
refused to run. Reading the declared tables instead means only a declaration counts,
and nested tables need no special case: TOML creates the parent implicitly, so
[tool.hatch.build] alone still declares hatch.

A file we cannot parse declares nothing, which leaves the project ambiguous and the
user asked — the honest answer when we cannot read what manages it.

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

* refactor(setup): read pyproject.toml once per detection

Detection parsed the file twice for a Python project: once for the detector's own
package manager and again for the confidence verdict. The verdict is now the single
source for the languages it models, and the detector leaves the field to it. A
language it does not model, such as Java's maven versus gradle, keeps the answer its
detector gives.

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

* fix(setup): stop misreading other ecosystems' install errors

The packageManager guidance matched a missing or non-semver version anywhere in an
install failure, so a gem, a Python package or a Go module reporting either phrase
had its real error replaced by advice about a file it does not have. The output has
to name package.json, which both corepack refusals do.

Say what was actually found when a project is set up for more than one manager: a
Pipfile and a [tool.*] table count as commitments too, so naming lockfiles sent the
reader looking for files that are not there.

Note on stderr when no --package-manager was given, since that used to fall through
to npm or pip and now reads the project. Callers parsing output are unaffected.

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

* feat(setup): ask which package manager to use when the project is ambiguous

A project with lockfiles for two managers, or none at all, has no answer we can
read off disk, and picking one is how a yarn project gets installed with npm. The
wizard now asks, and says why it is asking.

Installed managers are listed first and the cursor starts on one, but an
uninstalled manager stays selectable: the choice is the user's and the install
step already warns rather than installing tooling. Projects that state their
manager go straight to the plan.

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

* fix(setup): keep the package-manager picker inside the terminal

The screen draws a title, the reason for asking and a key hint around the list,
but the list was sized to the whole window, so the hint — including how to go
back — was pushed off the bottom at every terminal size. The list now leaves room
for that chrome, and the list's own help line goes away since the screen prints
its own. Below fourteen rows the reason is dropped: at that size the question and
the choices matter more than the explanation.

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

* fix(setup): show each screen's key hints once, inside the list

The project and environment screens printed a footer of key hints while the list
below already rendered its own help, so every instruction appeared twice. The
wizard's own bindings now go into the list's help line, which is the single place
a screen states them, and the footers are gone.

The package-manager picker follows the same shape, and its height reserve is
retuned for the help line the list now draws, including the extra row that line
takes when the terminal is narrower than it is.

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

* fix(setup): wrap the plan steps to the terminal width

A step naming an absolute entry-point path alongside the warning that no entry
file was found runs well past a narrow terminal, and overflowing there hides the
warning the step exists to give. Steps now wrap, with what wraps indented under
the number so a step still reads as one item. The screen that names the injected
file wraps for the same reason.

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

* fix(setup): stop padding pushing the injected file path off screen

Wrapping pads every line to the full width, so the newline left inside the
wrapped lead put a whole row of spaces in front of the file path and carried it
past the edge of the terminal. The newline now sits outside the wrap, and the
closing instruction wraps too.

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

* fix(setup): stop the package-manager list quitting on esc

The list inherits the same quit binding as the others, so esc arriving on its own
ended the session from the picker.

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

* feat(setup): say the verify step waits for the app as well as the SDK

Verification cannot succeed until the user's application is running, so naming
only the SDK left it unclear whether anything was expected of them.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ffantl-ld
ffantl-ld requested review from cspath1 and erangeles August 20, 2026 17:59
A successful install can leave the user something to do — the SDK not recorded in
requirements.txt — but only the verification-succeeded screen said so. The flow also
ends on "manual SDK setup required" and on a verification timeout, both reachable
after a real install, and on those the note was lost.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant