Skip to content

fix(cli): fail at start-up on a mismatched effect version instead of crashing inside alchemy - #202

Merged
wmadden-electric merged 5 commits into
mainfrom
claude/tml-3158-effect-preflight
Aug 4, 2026
Merged

fix(cli): fail at start-up on a mismatched effect version instead of crashing inside alchemy#202
wmadden-electric merged 5 commits into
mainfrom
claude/tml-3158-effect-preflight

Conversation

@wmadden-electric

@wmadden-electric wmadden-electric commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

A user with a slightly unlucky dependency tree sees this on 0.6.0, having done nothing wrong:

$ npx prisma-composer deploy app.ts
TypeError: Schedule.both is not a function
  at node_modules/alchemy/lib/AWS/IAM/SAMLProvider.js:33

They never asked for AWS IAM, and nothing in the message points at the real cause.

The decision

Check the installed dependency tree when the CLI starts, and refuse to run when it cannot work — so the same situation produces this instead:

$ npx prisma-composer deploy app.ts
Error: Dependency conflict: alchemy resolves effect@4.0.0-beta.103, but
@prisma/composer requires effect@4.0.0-beta.93. Your package manager installed
a second effect that alchemy picks up; deploying with it would crash inside
alchemy.
  Add to your package.json, then reinstall:
  "overrides": { "effect": "4.0.0-beta.93" }

Detecting the problem is as far as we can go — we cannot prevent it. That is the uncomfortable part, so it is worth explaining before the code.

What is actually going wrong

Composer deploys through alchemy, which is built on the effect library. alchemy 2.0.0-beta.59 works only with effect@4.0.0-beta.93; newer betas removed functions it calls (Schedule.both, Schedule.either). Composer pins that exact version.

A pin only constrains the edges we declare. In the reported tree the app itself depended on @effect/platform-node-shared@^4.0.0-beta.93. npm floated that to a newer beta, which requires a newer effect as a peer — so npm installed one, and placed it at the root of node_modules, where alchemy finds it first. Composer's pinned copy sat nested and unused. #196 pinned every package in that family exactly, which does fix the trees our packages fully own; this is not one of them, which is why TML-3158 is reopened.

We cannot close the hole from our side. alchemy declares effect >=4.0.0-beta.84, so a newer beta genuinely satisfies alchemy's own requirement and npm is behaving correctly. Only the app's own overrides can force which copy alchemy resolves — which is exactly what the new error asks for.

Changes

  • check-effect-resolution.ts (new): resolves effect from alchemy's installed location the way Node will at runtime, and compares it against Composer's own pin, read from the installed manifest so the version is never written down twice. When alchemy, Composer or effect cannot be resolved it skips rather than guesses, leaving unfamiliar layouts (pnpm-isolated, Yarn PnP, no alchemy installed) unaffected. Costs 1.7 ms cold, 0.2 ms warm, and touches no network.
  • bin.ts: runs the check before the CLI's command graph loads, behind a dynamic import. The ordering is forced — in a broken tree the import itself is what throws, so a check that runs any later never gets the chance.
  • scripts/check-npm-effect-resolution.mjs: a third install shape reproducing the reported tree, asserting the built CLI reports our error rather than the TypeError. "npm refused the install" counts as a pass only when npm actually failed on the dependency conflict — otherwise a registry outage would quietly turn the one end-to-end proof green. The two healthy shapes now also prove the published bin still starts.
  • Docs: the failure and its fix in docs/guides/deploying.md and skills/prisma-composer/SKILL.md.

Why every command, not just the deploying ones

The check first ran only for deploy, destroy and dev, on the assumption that only those load alchemy. The new CI shape disproved it: prisma-composer --help crashed with the same TypeError, because the command graph reaches alchemy's provider tree whatever the arguments say. The check now guards every command, which also settles any question about aliases or flag spellings slipping past it.

Alternatives considered

  • Declare effect as an exact peerDependency — the obvious candidate, on the theory that npm must then honour our version at the root or fail loudly. Tested against the reported tree: npm install exits 0, hoists the newer effect anyway, marks our peer edge invalid, and prints only warnings. Transitive peer conflicts do not hard-fail without --strict-peer-deps, which we cannot impose on consumers, and yarn classic does not auto-install peers at all. It would buy silent breakage plus extra scrollback.
  • Ship the overrides block in a template or init — helps new apps only, and does nothing for a tree that already exists, like this one. Worth doing separately.
  • Upgrade alchemy or effect — a newer pair floats the same way. The durable fix is upstream, alchemy tightening its own ranges; tracked as a follow-up.

🤖 Generated with Claude Code

…d effect

TML-3158 reopened on 0.6.0: the exact pins are not sufficient in real
trees. A consumer app that also depends on a floating @effect/* range
(the operator hit @effect/platform-node-shared@^4.0.0-beta.93, which
npm floats to beta.102) drags a newer effect peer to the root of
node_modules, over our pins, with only an npm warning. alchemy then
imports the newer copy and crashes mid-deploy
(`TypeError: Schedule.both is not a function`).

No dependency declaration can prevent this. Empirically verified in
scratch trees on npm 11.6.2: an exact effect peerDependency on our
packages does NOT make npm fail or keep the pinned version at root —
transitive peer conflicts are downgraded to warnings and the newer
copy is hoisted anyway. So the enforcement point moves to the CLI:

- check-effect-resolution.ts resolves effect from alchemy's installed
  position (walk up to node_modules/alchemy, realpath, createRequire)
  and compares it to the version @prisma/composer's own installed
  package.json pins (single source of truth — never hardcoded). On
  mismatch it fails with a short error naming found vs required and
  the consumer fix: `"overrides": { "effect": "<required>" }`. When
  either side cannot be determined (no alchemy, no composer, odd
  layout) the check skips instead of misfiring — later steps already
  report those states with their own errors. Verified not to misfire
  in this monorepo (hoisted pnpm layout resolves beta.93) or in
  healthy npm trees.
- bin.ts runs the check BEFORE loading the rest of the CLI, for the
  alchemy-driving commands only (deploy/destroy/dev): the command
  modules transitively import alchemy's provider tree, which crashes
  at import time in exactly the broken trees the check exists to
  explain — so cli.ts is now imported dynamically after the check.
  --help and log keep working in a broken tree.

Unit tests cover resolution, the healthy/unknown/mismatch rule, and
the operator's exact broken shape (root effect@beta.102, composer's
pin nested). Docs: the failure and its fix are documented in
docs/guides/deploying.md and skills/prisma-composer/SKILL.md.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Extends the standalone-install regression check with the tree that
broke 0.6.0 in the field: the scratch app also depends on
@effect/platform-node-shared@^4.0.0-beta.93, which npm floats to the
newest beta and hoists its newer effect peer over our exact pins —
install still exits 0 (verified; a peerDependency changes nothing).

Acceptance for that shape: npm refuses the install outright, OR the
install lands broken (alchemy resolves effect != the pin) and running
the built prisma-composer bin there must exit non-zero with the
start-up check's error ("alchemy resolves effect@..."), never the
Schedule TypeError. The two healthy shapes additionally assert the
inverse: the CLI check must NOT trip on a good tree.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…ing ones

The argv sniff assumed only deploy/destroy/dev load alchemy. The adversarial
npm shape disproves it: `prisma-composer --help` in a tree where alchemy
resolves a mismatched effect dies with the raw `Schedule.both is not a
function` from alchemy/lib/AWS/IAM/SAMLProvider.js, because the CLI module
graph reaches the provider tree whatever the argv says. Run the check for
every invocation, so no command can meet that TypeError.

The npm shape now asserts it: `--help` in the broken tree must report the
check, and the install-refused path is only accepted when npm actually failed
on the dependency conflict, so a registry outage cannot turn the one
end-to-end proof green. The healthy shapes assert the bin still reaches its
usage output — proof it ran at all, which "the marker is absent" alone does
not give. That assertion tests the banner rather than the exit code: a bare
`--help` exits 1 on main too, since clipanion reports it as a missing command.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@wmadden-electric, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 55 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fb4673a0-5ccb-4a6c-913e-2480a2fabaa6

📥 Commits

Reviewing files that changed from the base of the PR and between 7d452a8 and f55b02b.

📒 Files selected for processing (1)
  • docs/guides/deploying.md

Summary by CodeRabbit

  • New Features

    • Added a startup compatibility check for prisma-composer commands.
    • Detects incompatible dependency versions before execution and provides package-manager-specific remediation.
    • Applies consistently across commands, including deploy and --help.
  • Documentation

    • Added guidance on dependency conflicts, hoisting scenarios, override or resolution settings, and reinstall steps.
    • Documented the issue and recommended fixes in the Prisma Composer skill guide.
  • Tests

    • Expanded coverage for compatible, conflicting, missing, and nested dependency installations.

Walkthrough

The CLI now checks the Effect version resolved from Alchemy against Composer’s required version before loading commands. Conflicts produce a CliError with npm, Yarn, and pnpm remediation steps. Tests cover package discovery, nested resolution, missing packages, matching versions, and conflicts. The npm validation script checks healthy installations and adversarial dependency trees. Documentation describes the hoisting scenario and override configuration.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the CLI startup check for mismatched Effect versions and the resulting failure behavior.
Description check ✅ Passed The description directly explains the dependency conflict, the startup preflight, the remediation, and the related tests and documentation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/tml-3158-effect-preflight
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch claude/tml-3158-effect-preflight

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Aug 4, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@prisma/composer@202
npm i https://pkg.pr.new/@prisma/composer-prisma-cloud@202

commit: f55b02b

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/guides/deploying.md`:
- Around line 192-197: Add the `text` or `console` language tag to the fenced
diagnostic code block containing the dependency conflict output, preserving its
contents unchanged.

In
`@packages/0-framework/3-tooling/cli/src/__tests__/check-effect-resolution.test.ts`:
- Around line 158-160: Update both conflict-path assertions for
checkEffectResolution to verify that the thrown value is a CliError, not only
that its message matches the expected text. Preserve the existing message
patterns while adding the type assertion to both relevant expect blocks.

In `@scripts/check-npm-effect-resolution.mjs`:
- Around line 228-233: Update the adversarial resolution check around
resolvedVersion, the --help invocation, and the final success path so the script
fails when npm keeps pinnedEffect instead of producing the expected mismatch.
Require help.status to be non-zero regardless of CLI_CHECK_MARKER output, while
preserving marker validation for the failure output, and ensure the final
success message is reachable only after both assertions pass.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b07b8c26-2691-407f-8df4-b53acf33ed78

📥 Commits

Reviewing files that changed from the base of the PR and between ec391cd and d0dec87.

📒 Files selected for processing (6)
  • docs/guides/deploying.md
  • packages/0-framework/3-tooling/cli/src/__tests__/check-effect-resolution.test.ts
  • packages/0-framework/3-tooling/cli/src/bin.ts
  • packages/0-framework/3-tooling/cli/src/check-effect-resolution.ts
  • scripts/check-npm-effect-resolution.mjs
  • skills/prisma-composer/SKILL.md

Comment thread docs/guides/deploying.md Outdated
Comment thread scripts/check-npm-effect-resolution.mjs Outdated
@wmadden-electric wmadden-electric changed the title fix(cli): stop every command when alchemy resolves the wrong effect fix(cli): fail at start-up on a mismatched effect version instead of crashing inside alchemy Aug 4, 2026
…ror type

Review follow-ups. The adversarial fixture named a floating range, so the day
npm stopped producing a mismatch the shape would have passed while proving
nothing. It now names the exact @effect/platform-node-shared release whose
effect peer is newer than our pin, and treats "no mismatch" as a failure that
says which constant to move — reachable only when the pin itself moves past
that release, which is fixture maintenance rather than registry weather.

`--help` in the broken tree must also exit non-zero, not merely print the
error: a preflight that explains itself and exits 0 would let a script carry
on. The unit tests assert CliError rather than message text alone, since
bin.ts branches on that type to choose the exit status.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/guides/deploying.md (1)

205-210: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the Yarn and pnpm fix syntax for effect version conflicts.

Lines 205-210 provide copyable npm syntax only, then mention resolutions and pnpm.overrides for the other package managers without showing where those settings go. Add the Yarn and pnpm package.json entries and restate that dependencies must be reinstalled after adding the setting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/guides/deploying.md` around lines 205 - 210, Update the
dependency-override guidance in the deployment documentation to include copyable
Yarn `resolutions` and pnpm `pnpm.overrides` package.json entries for resolving
`effect` version conflicts, alongside the existing npm example. Explicitly
remind readers to reinstall dependencies after adding any of these settings.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@docs/guides/deploying.md`:
- Around line 205-210: Update the dependency-override guidance in the deployment
documentation to include copyable Yarn `resolutions` and pnpm `pnpm.overrides`
package.json entries for resolving `effect` version conflicts, alongside the
existing npm example. Explicitly remind readers to reinstall dependencies after
adding any of these settings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c9c6abe7-b19c-4651-98b6-65a9e30c95be

📥 Commits

Reviewing files that changed from the base of the PR and between d0dec87 and 7d452a8.

📒 Files selected for processing (3)
  • docs/guides/deploying.md
  • packages/0-framework/3-tooling/cli/src/__tests__/check-effect-resolution.test.ts
  • scripts/check-npm-effect-resolution.mjs

Naming `resolutions` and `pnpm.overrides` without showing them left readers
on those package managers to guess the nesting, which pnpm gets wrong in a
way that fails silently.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric

Copy link
Copy Markdown
Contributor Author

Addressed the outside-diff-range comment on docs/guides/deploying.md in f55b02b — the guide now shows the yarn resolutions and pnpm pnpm.overrides entries as copyable JSON rather than naming them, and states that a reinstall is required. Worth doing: the pnpm form nests under a pnpm key, which is easy to get wrong and fails silently when you do.

@wmadden-electric
wmadden-electric merged commit d30d751 into main Aug 4, 2026
22 of 23 checks passed
@wmadden-electric
wmadden-electric deleted the claude/tml-3158-effect-preflight branch August 4, 2026 09:56
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