Skip to content

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

Merged
ffantl-ld merged 3 commits into
setup-ldfrom
fix/setup-rederive-on-override
Aug 20, 2026
Merged

fix(setup): re-derive entry point and package manager on SDK override#782
ffantl-ld merged 3 commits into
setup-ldfrom
fix/setup-rederive-on-override

Conversation

@ffantl-ld

@ffantl-ld ffantl-ld commented Aug 17, 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

REL-15430 — found in the ldcli setup bug bash. Also closes known issues #9 (package manager not re-derived) and #8 (empty file path on the final screen) from the bug bash page.

Stacked on #781, which targets setup-ld (#776) and touches the same files.

Describe the solution you've provided

  • Re-derive the entry point and the package manager for the chosen SDK when the user overrides detection, instead of substituting that SDK's bare default in $PWD. Previously a project whose entry file was src/index.js got a second index.js created beside it, and the package manager still described the language detection guessed first — so a Ruby install never ran bundle add.
  • Move the entry-point candidate lists into one table that both detection and the override path read, so the two cannot disagree about where code goes. Framework-specific layouts (Next.js, React) stay inline, since an override clears the detected framework.
  • Omit the destination from the "add this initialization code" line when the SDK only shows a snippet and has no entry point, rather than printing an empty path.

Describe alternatives you've considered

  • Keeping the per-SDK default and only fixing the file-exists check. Rejected: the default is a single filename, so it still misses an entry point that lives anywhere other than the repo root.
  • Preserving the detected package manager, which the previous test asserted as intended behaviour. Rejected: pnpm cannot install a gem, and carrying it forward is what suppressed bundle add.

Additional context

TestWizard_OverrideSDK_DoesNotReuseDetectedEntryPoint asserted that the package manager survives an override, which encoded known issue #9 as expected behaviour. Its expectation is updated to the re-derived value.

Testing approaches

  • go test ./... passes.
  • Added unit coverage for EntryPointFor (finds an existing file, suggests a fallback, empty for snippet-only SDKs) and PackageManagerFor (Bundler vs bare gem, node lockfile, uv, manual-install SDKs).
  • Added a wizard test reproducing the reported shape: a project with only src/index.js, overridden to node-server, now resolves to the existing file rather than creating a sibling.
  • Confirmed with a built binary that setup detect reports src/index.js for that project shape. The override itself is TUI-only and is covered by the model-level test rather than by hand.

Note

Overview
Stops guessing the package manager when the project is silent or contradictory, and re-derives entry point and manager when the user overrides the detected SDK (so a Node src/index.js is not replaced by a sibling index.js, and a Ruby override no longer keeps pnpm).

Detection now returns confidence plus a reason. Node honors a valid Corepack packageManager field over lockfiles; Python parses pyproject.toml [tool.*] tables (including nested ones) and adds pdm. Conflicting lockfiles, hatch-only projects, and invalid Corepack specs are ambiguous. Shared EntryPointFor / PackageManagerChoiceFor keep detect and override on the same tables.

Wizard inserts a package-manager picker only when ambiguous (installed tools first; missing ones still selectable). setup install without --package-manager uses that verdict, errors if ambiguous, and prints a one-time note on stderr. Failed Corepack specs get a dedicated install reason. Plan/wait screens wrap so long paths and key hints stay on screen.

Reviewed by Cursor Bugbot for commit 1bd7402. 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 17, 2026 13:06
@ffantl-ld
ffantl-ld force-pushed the fix/setup-rederive-on-override branch 2 times, most recently from 75ca625 to 3e622b5 Compare August 17, 2026 16:58
@ffantl-ld
ffantl-ld force-pushed the fix/setup-rederive-on-override branch 2 times, most recently from 47efc96 to 4129348 Compare August 18, 2026 15:29
@ffantl-ld
ffantl-ld force-pushed the fix/setup-resolve-python-pip branch from 3ceb77b to 2ed8a45 Compare August 19, 2026 14:12
@ffantl-ld
ffantl-ld force-pushed the fix/setup-rederive-on-override branch 2 times, most recently from b94d3d1 to 88af10e Compare August 19, 2026 15:51
@ffantl-ld
ffantl-ld force-pushed the fix/setup-resolve-python-pip branch from 7798f33 to 1797852 Compare August 19, 2026 16:08
@ffantl-ld
ffantl-ld force-pushed the fix/setup-rederive-on-override branch from 88af10e to 9c8a11b Compare August 19, 2026 16:08
@ffantl-ld
ffantl-ld force-pushed the fix/setup-resolve-python-pip branch from 1797852 to 046e70c Compare August 19, 2026 16:52
@ffantl-ld
ffantl-ld force-pushed the fix/setup-rederive-on-override branch 2 times, most recently from 1777210 to d8a6410 Compare August 19, 2026 17:39
@ffantl-ld
ffantl-ld force-pushed the fix/setup-rederive-on-override branch 3 times, most recently from b3c3950 to 9138af9 Compare August 20, 2026 15:13
@ffantl-ld
ffantl-ld force-pushed the fix/setup-resolve-python-pip branch from c9f1d48 to f3b25d1 Compare August 20, 2026 17:18
@ffantl-ld
ffantl-ld force-pushed the fix/setup-rederive-on-override branch from 9138af9 to e76ec60 Compare August 20, 2026 17:18
Base automatically changed from fix/setup-resolve-python-pip to setup-ld August 20, 2026 17:30
ffantl-ld and others added 3 commits August 20, 2026 13:35
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>
…ntly (#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>
…iguous (#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>
@ffantl-ld
ffantl-ld force-pushed the fix/setup-rederive-on-override branch from 78c9dc9 to 1bd7402 Compare August 20, 2026 17:36

@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 1bd7402. Configure here.

Comment thread cmd/setup/install.go
@ffantl-ld
ffantl-ld merged commit 3442ad4 into setup-ld Aug 20, 2026
6 checks passed
@ffantl-ld
ffantl-ld deleted the fix/setup-rederive-on-override branch August 20, 2026 17:41
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