Skip to content

fix(create-objectstack,cli): a clean first install no longer reports two unmet peers - #10407

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-10326-peer-warning-clean-install
Aug 20, 2026
Merged

fix(create-objectstack,cli): a clean first install no longer reports two unmet peers#10407
os-warren merged 1 commit into
mainfrom
claude/issue-10326-peer-warning-clean-install

Conversation

@os-warren

@os-warren os-warren commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Fixes #10326

A brand-new scaffold's very first pnpm install reported two unmet peer dependencies. Nothing was broken — but it is the one screen where a newcomer is deciding whether this project is solid, and there is nothing they did to cause it or can do about it.

Premise: reproduced before anything changed

Clean install means: scaffolded fresh from published create-objectstack@17.1.0, then rm -rf node_modules pnpm-lock.yaml and pnpm install again — no lockfile, no node_modules, nothing configured by the user. The pnpm in this container is 11.22.0, which moved the per-package lines out of the install summary and behind pnpm peers check; the filing's inline ✕ unmet peer … shape is pnpm 10's rendering of the same two facts.

Issues with peer dependencies found

✕ unmet peer better-call
  Installed: 1.4.0
  Wanted:
    1.3.7:
      @better-auth/scim@1.7.0-rc.1

✕ unmet peer better-sqlite3
  Installed: 13.0.3
  Wanted:
    ^12.0.0:
      better-auth@1.7.1

A second scaffold path reproduces both, identically, and the filing did not name it: objectstack init (packages/cli) renders its own pnpm-workspace.yaml. Same two lines, same versions.

① better-sqlite3 — the pin is right; the upstream range is stale, so it is widened

The question is behavioural, not textual, so it was measured rather than read off the range string.

What the range actually governs. better-auth 1.7.1 declares better-sqlite3 as an optional peer (peerDependenciesMeta.better-sqlite3.optional === true) at ^12.0.0. It governs exactly one configuration: a raw better-sqlite3 Database handed to better-auth's database option, which its Kysely dialect then drives. ObjectStack never takes that pathAuthManager.createDatabaseConfig() returns createObjectQLAdapterFactory(this.config.dataEngine), and all 14 better-sqlite3 mentions under plugin-auth are knex's client: 'better-sqlite3' underneath ObjectQL (the grep was controlled against a known-present neighbour term in the same files).

Measured on the configuration it does govern. A probe built better-auth 1.7.1 with database: new Database(':memory:') and drove it end to end:

MEASURED better-sqlite3 = 13.0.3 | better-auth = 1.7.1 | better-auth peer range = ^12.0.0
MIGRATIONS: ok — tables = account,session,user,verification
SIGNUP: ok — user id = c7lwqlSTpVX8MwoKHY8SyrsCO1fajorb email = probe@example.com
READBACK FROM better-sqlite3 13: {"id":"c7lwqlSTpVX8MwoKHY8SyrsCO1fajorb","email":"probe@example.com"}
SIGNIN: ok — token present = true
ADAPTER findOne: ok — probe@example.com
ADAPTER update: ok — name = Renamed
ADAPTER delete: ok — remaining users = 0

Two controls make that green mean something:

  • Falsification. The same probe with Database.prototype.prepare neutered fails, in kysely's SqliteIntrospector — so the green is better-sqlite3 actually executing, not an unexercised path.
  • The version the range names. The identical probe on better-sqlite3 12.11.1 produces the identical transcript. 13 is not merely tolerated; it is indistinguishable from what the range asks for.

Therefore: widen, do not correct the pin. Pinning our own ^13.0.3 declarations (driver-sql, driver-turso, plugin-sharing, cli) back to ^12 would downgrade a native module across the whole platform to satisfy a range that measurement shows is simply behind. The remedy is pnpm's peerDependencyRules.allowedVersions, which is the only way to widen a third-party peer range we do not own.

② @better-auth/scim — the rc pin stays; this card stops at the reading

The npm view reading, verbatim, taken today:

$ npm view @better-auth/scim dist-tags
{
  'release-1.4': '1.4.22',
  beta: '1.7.0-beta.10',
  rc: '1.7.0-rc.6',
  latest: '1.7.1'
}

latest is a stable, non-rc version. No migration is performed here. Stable 1.7.x ships the rc.2 whole-model rewrite, so adopting it is an architecture card of its own, not a version bump — and the pin at 1.7.0-rc.1 is deliberate, not a decayed version stamp.

On the warning itself: the rc peers an exact better-call@1.3.7 while better-auth 1.7.1 depends on 1.4.0. A better-auth plugin has to share the host's better-call instance, so the single 1.4.0 copy every install already resolves is the correct tree rather than a skew to repair. Measured: exactly one better-call version in the lockfile (better-call@1.4.0), with scim's node_modules/better-call symlinked to it — in the scaffolded downstream tree and in this repo's own lockfile alike. The declaration retires together with the rc pin.

A correction to the workspace prose, in scope because it is this half's own evidence. pnpm-workspace.yaml's scim note claimed "so the tree carries two copies". It does not, and never did in the tree as resolved. That sentence is corrected in place, with the measurement, so the next author doing the SCIM migration is not working from a false picture. The root file's diff is comment-only — verified by filtering the diff to non-# lines, which comes back empty; the pin line itself does not appear in the diff at all.

What changed

Both remedies are peerDependencyRules.allowedVersions entries, keyed with pnpm's scoped spelling (the declaring package's name, then a greater-than sign, then the peer's name) so each widens exactly one declaration — a bare better-sqlite3: '13' would silence that peer for every package declaring it, including one whose complaint would be real. A test pins that scoping in both packages.

They ship inside the scaffold, not in this repo's workspace file, and that placement is load-bearing: a peerDependencyRules block here would not travel with published packages — the same limitation the overrides block already documents, and the reason plugin-auth restates its ranges. So the entries land in the two files a newcomer actually gets:

  • packages/create-objectstack/src/templates/blank/pnpm-workspace.yaml
  • renderPnpmWorkspaceYaml() in packages/cli/src/commands/init.ts (data-driven from an exported SCAFFOLD_ALLOWED_PEER_VERSIONS; an empty map emits no block at all, rather than a dangling header advertising a declaration that is not there)

Verified gone on the same clean-install path that produced them. Both scaffolds, both re-installed from zero:

$ pnpm peers check
No peer dependency issues found            # exit 0

and the install summary no longer carries [WARN] Issues with peer dependencies found.

Lockfile blast radius: zero, measured both ways

allowedVersions changes what pnpm reports, never what it resolves. Rather than assert that, both scaffolds were installed from scratch with and without the block and the lockfiles diffed:

scaffold diff lines between baseline and fixed lockfile
create-objectstack@17.1.0 0
objectstack init 0

Nothing moved beyond the two named packages because nothing moved at all — no version, no range, no resolution, in the scaffolds or in this repo (whose lockfile this PR does not touch).

One thing deliberately not done

This repo's own pnpm-workspace.yaml gets no peerDependencyRules block. It does not show these warnings, because auto-install-peers=true in the root .npmrc quietly installs a second, entirely unused better-sqlite3@12.11.1 to satisfy the stale peer — which is exactly why CI never saw what a scaffolded project shows on its first screen. Adding the rule here was tried and measured: it moves 0 lockfile lines today, so it would sit inert until some unrelated future re-resolve dropped the duplicate as unreviewed drift in this repo's hottest merge-conflict file. That masking is recorded in the workspace comment instead, where the next reader will find it.

Verification

Reverse verification, run from the committed state. Both suites read source, not distinit.test.ts imports ../src/commands/init by relative path and the template test reads src/templates/… off disk, neither resolving through a package's exports — so no rebuild is involved and none was needed. Predicted direction before running: the declarations' removal reds the new assertions, except the "emits nothing when given no rules" test, which should stay green because an empty map genuinely should emit nothing. Observed, exactly:

leg packages/cli create-objectstack
declarations removed 4 failed | 33 passed (37) 3 failed | 21 passed (24)
restored from the commit 37 passed (37) 24 passed (24)

Targeted suites and typechecks, script names echoed in the output (so no zero-match silent pass):

  • pnpm --filter create-objectstack testTest Files 7 passed (7), Tests 84 passed (84)
  • pnpm --filter @objectstack/cli exec vitest run test/init.test.tsTests 37 passed (37)
  • pnpm --filter @objectstack/cli typecheck and pnpm --filter create-objectstack typecheck — both clean

Gate union — node scripts/pm/dispatch-gates.mjs, no paths passed

Derived after the final commit on a clean worktree at 83e1c328c, exit codes captured before any pipe. 20 families run, 19 exit 0. The one non-zero is pre-existing and provably not mine:

EXIT=1 :: node scripts/check-prerelease-pin-watch.mjs

Its own verdict line reads ⛔ A STABLE release is available for 1 prerelease pin(s) — the trigger condition of #3653 has ARRIVED. That is the standing SCIM watch firing on the npm view reading quoted above, and it is red on main for the same reason: this PR's only change to pnpm-workspace.yaml is comment text, and the scim pin line is not in the diff.

Green, each quoted from its own exit code: check:nul-bytes, check:changeset-gate-self-tests, check:cross-package-test-inputs, check:objectui-changeset, check:override-consistency, check:slot-lookup, check:template-version-sync, check-adr-0087-registration.mjs, check-changeset-fixed.mjs, check-changeset-no-major.mjs, check-cross-package-test-inputs.mjs, check-empty-changeset.mjs, check-osv-exemptions.mjs, docs-audit/check-affected-docs.mjs, plus the convention-triggered set the two edited test files move: check:query-options-erasure, check:type-check-coverage, check:engine-double-contract, check:where-matcher, and check:type-check-debt — the last run against a fully built workspace closure (turbo run build, 70 successful, 70 total), reporting 33 ledger entr(ies) re-measured in 236.8s, 1924 raw tsc error(s) total, none above its recorded number and surplus: none. No ledger entry was raised or touched.

A note on where the CLI test edits are typechecked: packages/cli/tsconfig.json includes only src, so test/** sits outside that program — pre-existing, documented debt already carried in the type-check-coverage ledger (packages/cli is named in the script's own header). The additions were checked directly against the file anyway and introduce no error naming a line this PR wrote.

Changeset

Shipped, and it is the gate's call rather than habit: check-empty-changeset.mjs / check-changeset-fixed.mjs are in this card's derived gate union, and the change is user-visible — it alters what published create-objectstack and @objectstack/cli write into a new project. patch on both.

Out of scope, filed not fixed

objectstack init's scaffold hard-fails pnpm install on pnpm 11 with ERR_PNPM_IGNORED_BUILDS (exit 1), because renderPnpmWorkspaceYaml emits only onlyBuiltDependencies and not the allowBuilds key that pnpm 11 reads — the same defect class the blank template already fixed for itself. It is a different defect from this card's (an install that fails outright, not a warning on an install that succeeds), so it is filed rather than ridden. See #10405; that issue is not addressed here.


Generated by Claude Code

…n first install reports

A brand-new scaffold's very first `pnpm install` printed two unmet-peer lines —
`better-auth 1.7.1` wanting `better-sqlite3@^12.0.0` against the resolved
13.0.3, and `@better-auth/scim@1.7.0-rc.1` wanting an exact `better-call@1.3.7`
against the resolved 1.4.0. Reproduced on published `create-objectstack@17.1.0`
and again on `objectstack init`, both with no lockfile and no node_modules.

better-sqlite3: the peer is OPTIONAL and governs only a raw better-sqlite3
`Database` handed to better-auth's `database` option — a path ObjectStack never
takes (AuthManager passes an ObjectQL adapter factory). Measured on the path it
does govern, better-auth 1.7.1 behaves identically on 13.0.3 and 12.11.1, so
the upstream range is stale: widen it, do not pin ours back to ^12.

@better-auth/scim: `npm view` shows a stable latest, but stable 1.7.x ships the
rc.2 whole-model rewrite, so the exact rc pin stays and its adoption remains a
separate migration. A better-auth plugin must share the HOST's better-call
instance, so the single 1.4.0 copy every install resolves is the correct tree.

Both are pnpm `peerDependencyRules.allowedVersions` entries, scoped
`<declaring package>><peer>`, shipped inside the scaffold because such a block
in this repo's own workspace file does not travel with published packages.
`allowedVersions` changes what pnpm REPORTS, never what it resolves: measured
on both scaffold paths the lockfile is byte-identical, 0 lines of diff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnJHU45vPJj5UQrxe946Bx
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, objectstack-blank, touching 3 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx (via os init (command))
  • content/docs/getting-started/examples.mdx (via os init (command))
  • content/docs/getting-started/your-first-project.mdx (via os init (command))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx (via os init (command))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/create-objectstack/src/templates/blank/pnpm-workspace.yaml) — pages documenting those are invisible to this run
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json fda3d67f99a14e1e3ccb8283ee9e4665ac3f1c84packageMentionDocs.

Which tree this was computed on

This run read content/docs from e6deea5a4f21b9574c1461c60caf0cd56b08e8f5 — the merge of head 83e1c328c527ad796a4bf1844908afa69e6fa53f into base fda3d67f99a14e1e3ccb8283ee9e4665ac3f1c84, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e6deea5a4f21b9574c1461c60caf0cd56b08e8f5 && git checkout e6deea5a4f21b9574c1461c60caf0cd56b08e8f5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fda3d67f99a14e1e3ccb8283ee9e4665ac3f1c84 83e1c328c527ad796a4bf1844908afa69e6fa53f && git checkout -B drift-repro fda3d67f99a14e1e3ccb8283ee9e4665ac3f1c84 && git merge --no-ff 83e1c328c527ad796a4bf1844908afa69e6fa53f

node scripts/docs-audit/affected-docs.mjs --json fda3d67f99a14e1e3ccb8283ee9e4665ac3f1c84

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs fda3d67f99a14e1e3ccb8283ee9e4665ac3f1c84 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 20, 2026
@os-warren
os-warren marked this pull request as ready for review August 20, 2026 18:24
@os-warren
os-warren enabled auto-merge August 20, 2026 18:24
@os-warren
os-warren added this pull request to the merge queue Aug 20, 2026
Merged via the queue into main with commit 675ab57 Aug 20, 2026
35 checks passed
@os-warren
os-warren deleted the claude/issue-10326-peer-warning-clean-install branch August 20, 2026 18:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] A clean first install shows the newcomer two red ✕ unmet-peer warnings (better-sqlite3, better-call)

2 participants