fix(cli): pin init's installs to the release's own package versions (#661)#666
Conversation
…661) `stash init` installed runtime packages unpinned, resolving through npm dist-tags — which lag or point at placeholders during a pre-release window. The 2026-07-16 skilltester run showed the failure mode end to end: a bare `npm install` delivered `@cipherstash/stack@0.19.0` + `stack-drizzle@0.0.0` instead of rc.1, breaking `/v3` imports and silently making two eval surfaces test the wrong release. Embed the release train's exact versions at build time (tsup define, same mechanism as the PostHog key, but set on every build since it needs no env var; a missing workspace manifest fails the build rather than silently degrading to unpinned). `stash init` now installs `@cipherstash/stack`, the integration adapter, and `stash` itself pinned to those versions, and warns — never mutates — when an already-installed package's resolved node_modules version differs from the release's, printing the exact align command. The install guidance in missing-package hints and context.json is pinned the same way. Source-mode runs (unit tests, tsx) see no embed and keep today's bare names. Skill note added to stash-cli (init pins; treat the skew warning as real). Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
🦋 Changeset detectedLatest commit: 67dcd0b The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
📝 WalkthroughWalkthroughThe CLI now embeds release-train package versions at build time, pins installation and recovery guidance to those versions, detects installed version skew during ChangesRuntime Version Pinning
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant CLI
participant stash_init
participant installDepsStep
participant node_modules
participant PackageManager
CLI->>stash_init: start initialization
stash_init->>installDepsStep: resolve package requirements
installDepsStep->>node_modules: read installed package manifests
installDepsStep->>PackageManager: install pinned specs or align skew
installDepsStep->>CLI: report completion or skew commands
Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@packages/cli/src/bin/main.ts`:
- Line 85: Add an E2E CLI regression covering the missing-package guidance
emitted by the install hint using prodInstallCommand and pinnedSpec for
`@cipherstash/stack`. Assert the user-facing output includes the expected
installation command, preserving coverage for this exact guidance.
In `@packages/cli/src/commands/init/steps/install-deps.ts`:
- Around line 62-80: Update warnOnVersionSkew so skewed packages are split into
production and development groups before calling combinedInstallCommands,
preserving CLI_PACKAGE (stash) as a dev dependency. Reuse the file’s existing
package-role classification and generate the alignment command with separate
prod and dev pinned-spec arrays at both warnOnVersionSkew call sites.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: dd524734-63d9-4bc9-a46a-298c237fa0f3
📒 Files selected for processing (11)
.changeset/init-pins-runtime-versions.mdpackages/cli/src/__tests__/runtime-versions.test.tspackages/cli/src/bin/main.tspackages/cli/src/commands/init/lib/write-context.tspackages/cli/src/commands/init/steps/__tests__/install-deps.test.tspackages/cli/src/commands/init/steps/install-deps.tspackages/cli/src/commands/init/utils.tspackages/cli/src/config/missing-package.tspackages/cli/src/runtime-versions.tspackages/cli/tsup.config.tsskills/stash-cli/SKILL.md
There was a problem hiding this comment.
Pull request overview
This PR fixes stash init installing runtime dependencies via unpinned dist-tags by embedding the exact release-train package versions at build time and using those versions across init + install guidance, with an additional warning when already-installed packages are version-skewed from the CLI’s expected versions.
Changes:
- Embed release-train versions into the CLI build (
__STASH_RUNTIME_VERSIONS__) and expose helpers (expectedVersion,pinnedSpec). - Update
stash initand other install guidance paths to use pinned install specifiers and to warn on detected version skew. - Update
skills/stash-clidocumentation and add a Changeset entry for the CLI behavior change.
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| skills/stash-cli/SKILL.md | Documents pinned installs + version-skew warning behavior. |
| packages/cli/tsup.config.ts | Embeds sibling workspace package versions into the build via define. |
| packages/cli/src/runtime-versions.ts | New module that parses embedded versions and generates pinned install specs. |
| packages/cli/src/config/missing-package.ts | Pins install guidance for missing-package errors. |
| packages/cli/src/commands/init/utils.ts | Adds installedVersion() to read resolved node_modules version. |
| packages/cli/src/commands/init/steps/install-deps.ts | Pins installs and adds version-skew detection + warning output. |
| packages/cli/src/commands/init/steps/tests/install-deps.test.ts | Extends init install-deps tests for pinning + skew scenarios. |
| packages/cli/src/commands/init/lib/write-context.ts | Pins installCommand written to .cipherstash/context.json. |
| packages/cli/src/bin/main.ts | Pins install hint when @cipherstash/stack is missing. |
| packages/cli/src/tests/runtime-versions.test.ts | Tests source-mode fallback + explicit map behavior. |
| .changeset/init-pins-runtime-versions.md | Changeset describing pinned installs + skew warnings. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The mocked version maps used realistic values (1.0.0-rc.2), which read as if they must track an actual release. They are arbitrary fixtures — the tests assert the map is threaded through verbatim; production values come from the workspace manifests at build time, never from constants. Use 9.9.9-test.N so no one mistakes them for release-coupled values, and say so in comments. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
auxesis
left a comment
There was a problem hiding this comment.
Verdict
Solid, well-scoped change. The pinning logic itself is thoroughly tested where it's mocked — versionSkew, the pinned prod/dev arrays for fresh installs, the skew-warn-vs-silent paths, and the source-mode/explicit-map fallback in runtime-versions.test.ts. Every consolidated finding is a test gap, not a correctness bug: three new pieces of real parsing/error-handling code (installedVersion, the build-time embed in embeddedVersions, and the pinned missing-package guidance) run untested because tests reach them only through mocks. Both installedVersion and embeddedVersions fail closed, so the risk is regression-silence, not a live defect.
Review stats
| Source | Raw | Survived |
|---|---|---|
| claude (claude-opus-4-8) [test-gap] | 3 inline + 1 body | 3 inline kept, 1 body → overflow |
| codex (gpt-5.5) [test-gap] | 3 | 3 |
After de-duplication: 4 unique inline findings + 1 overflow. Cross-model overlap: 2 of the 4 kept findings were corroborated by both models (installedVersion untested; the build-time embed parser untested — codex anchored it at the RUNTIME_PACKAGE_VERSIONS export, claude at embeddedVersions; same issue). The other two are single-source but verified.
Additional findings not posted inline
- [single-source: claude] Post-install mixed-skew scenario untested (
packages/cli/src/commands/init/steps/install-deps.ts:205). The post-installwarnOnVersionSkew(allPackages)call site is only reached withinstalledVersionmocked toundefined, so a mixed run — one package freshly (pinned) installed while another was already present at a stale version — never fires a warning in any test. A test withisPackageInstalledreturning true for the integration pkg and false→true for stack, plusinstalledVersionreturning a stale version for the pre-existing one, would cover that branch.
…fail-loud embed, remaining unpinned surfaces Addresses the code-review findings on #666 plus reviewer comments (auxesis, Copilot, CodeRabbit): - Skew is surfaced BEFORE any prompt/exit, so decline and partial-failure paths can no longer skip it; interactively init offers alignment in the same confirm (stash stays in the DEV list — the align command previously put every skewed package in the prod slot, which npm/yarn-classic would use to move the CLI into dependencies). - An installed package with an unreadable manifest (aborted install) is reported as skew instead of silently passing the check. - A present-but-malformed version embed now THROWS instead of degrading to unpinned installs; parseEmbeddedVersions is exported and unit-tested, and a new e2e asserts the BUILT bundles carry the workspace versions (the behavioural guidance path is unreachable under the harness — jiti resolves the monorepo's own node_modules). - Release-train list is now a single shared source (src/release-train.ts) consumed by tsup and cross-checked by a unit test against INTEGRATION_ADAPTER_PACKAGES — a future adapter missing from the train fails tests instead of installing unpinned and skew-exempt. - Remaining unpinned surfaces pinned: `stash wizard` one-shot spawn (was executing bare @cipherstash/wizard; now on the train), install-eql's manual note, native.ts recovery hint (was stash@latest). - Sibling skills' bare install commands annotated with the pinning / verify-resolved guidance (stash-encryption, -supabase, -drizzle, -dynamodb). - New tests: parseEmbeddedVersions matrix, real-filesystem installedVersion, reportMissingCipherStashPackage pinned output, release-train coverage, skew decline/non-interactive/unreadable paths, dist embed e2e. Not addressed here (discussion): the release-train coupling itself — a stack-only release post-GA would leave the published stash embed stale and the skew warning advising downgrades. Needs a changesets fixed/linked group or an internal dep edge; tracked on the PR. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
|
Pushed @auxesis's four coverage gaps — all closed:
Copilot:
CodeRabbit (E2E for the missing-package guidance): the behavioural route is unreachable under the pty/e2e harness — jiti resolves Also in this push (from the internal review):
Known open question (deliberately not in this PR): post-GA, a stack-only release doesn't republish 487 unit + 58 e2e green. |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/cli/src/commands/init/steps/install-deps.ts (1)
262-281: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winDo not report success while alignment failed or skew remains.
An alignment command can fail while every package remains present at its old version. The presence-only check then logs
Stack dependencies installed.and returns successful flags despite unresolved skew. Re-runversionSkew(allPackages)and require no failed commands or remaining mismatches before reporting success.🤖 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 `@packages/cli/src/commands/init/steps/install-deps.ts` around lines 262 - 281, Update the post-alignment success check around stackInstalled, cliInstalled, and integrationInstalled to also re-run versionSkew(allPackages). Report success and return successful flags only when all packages are present, no alignment commands failed, and no version mismatches remain; otherwise preserve the missing/skew warning path.
🤖 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 `@packages/cli/src/commands/init/steps/install-deps.ts`:
- Around line 214-221: Update the non-interactive branch in the
install-dependency flow around combinedInstallCommands so it also prints the
alignment commands when alignment skew exists alongside missing packages.
Preserve installing only the missing packages, while ensuring the promised exact
remediation commands are displayed for mixed missing/skew runs.
In `@skills/stash-drizzle/SKILL.md`:
- Around line 27-33: Update the manual-install verification commands in
skills/stash-drizzle/SKILL.md lines 27-33 and skills/stash-supabase/SKILL.md
lines 34-40 to verify both first-party package versions: retain
`@cipherstash/stack` and add `@cipherstash/stack-drizzle` for the Drizzle guide and
`@cipherstash/stack-supabase` for the Supabase guide.
---
Outside diff comments:
In `@packages/cli/src/commands/init/steps/install-deps.ts`:
- Around line 262-281: Update the post-alignment success check around
stackInstalled, cliInstalled, and integrationInstalled to also re-run
versionSkew(allPackages). Report success and return successful flags only when
all packages are present, no alignment commands failed, and no version
mismatches remain; otherwise preserve the missing/skew warning path.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5119ef28-cd01-4c48-b07e-06a299c7bfd0
📒 Files selected for processing (18)
.changeset/init-pins-runtime-versions.mdpackages/cli/src/__tests__/release-train.test.tspackages/cli/src/__tests__/runtime-versions.test.tspackages/cli/src/commands/init/__tests__/utils.test.tspackages/cli/src/commands/init/steps/__tests__/install-deps.test.tspackages/cli/src/commands/init/steps/install-deps.tspackages/cli/src/commands/init/steps/install-eql.tspackages/cli/src/commands/wizard/index.tspackages/cli/src/config/__tests__/missing-package.test.tspackages/cli/src/native.tspackages/cli/src/release-train.tspackages/cli/src/runtime-versions.tspackages/cli/tests/e2e/runtime-versions-embed.e2e.test.tspackages/cli/tsup.config.tsskills/stash-drizzle/SKILL.mdskills/stash-dynamodb/SKILL.mdskills/stash-encryption/SKILL.mdskills/stash-supabase/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (3)
- .changeset/init-pins-runtime-versions.md
- packages/cli/tsup.config.ts
- packages/cli/src/commands/init/steps/tests/install-deps.test.ts
Closes the remaining #666 review finding: post-GA, a stack-only release would not republish stash (stack is only a devDep + in-range optional peer), leaving the published CLI's embedded version map stale — fresh installs would pin outdated versions and the skew warning would print downgrade commands to users on legitimately newer versions. Two-part fix: 1. Changesets `fixed` group over the release train (stash, stack, both adapters, wizard): a release of any republishes all at the same version, so the embed can never disagree with what's live. prisma-next stays on its own line by design. NOTE: wizard joins the group from 0.5.0-rc.1 and will land on the group version (1.0.0-rc.2) at the next `changeset version`. 2. Direction-aware skew: versionSkew classifies behind vs ahead using a small prerelease-aware comparator (compareVersions — deliberately not a full semver impl; no new dependency per the supply-chain policy). Behind (and unreadable) installs get the align offer as before; an install NEWER than this release expects gets "update stash to the matching release" and never a downgrade command. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
Closes the remaining #666 review finding: post-GA, a stack-only release would not republish stash (stack is only a devDep + in-range optional peer), leaving the published CLI's embedded version map stale — fresh installs would pin outdated versions and the skew warning would print downgrade commands to users on legitimately newer versions. Two-part fix: 1. Changesets `fixed` group over the release train (stash, stack, both adapters, wizard): a release of any republishes all at the same version, so the embed can never disagree with what's live. prisma-next stays on its own line by design. NOTE: wizard joins the group from 0.5.0-rc.1 and will land on the group version (1.0.0-rc.2) at the next `changeset version`. 2. Direction-aware skew: versionSkew classifies behind vs ahead using a small prerelease-aware comparator (compareVersions — deliberately not a full semver impl; no new dependency per the supply-chain policy). Behind (and unreadable) installs get the align offer as before; an install NEWER than this release expects gets "update stash to the matching release" and never a downgrade command. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
…ss (rc.2 M4) Non-interactive `stash init` reported success for setups that didn't fully complete. Three honesty gaps closed: 1. Version skew. A non-interactive run can't reconcile a `behind` skew (it won't mutate an install without consent), so instead of warning-and- proceeding — scaffolding against packages older than this CLI expects and then claiming success — it now REFUSES with a non-zero exit (CliExit(1)) and the exact align command. The #661/#666 no-mutation principle is preserved (execSync still not called); only the post-warning action changes from proceed to fail. `ahead` skew stays a warn (install likely fine; update the CLI). 2. False 'Setup complete'. The summary header and the auth/db/scaffold checkmarks were unconditional. Now: when EQL is required but not installed (eqlInstalled=false AND integration !== prisma-next), the summary reads 'Setup incomplete' and init exits non-zero, pointing at `stash eql install`. 'Database connection verified' → 'Database URL resolved' (init resolves a URL, never opens a connection). 'Encryption client scaffolded' is shown only when a client was written (clientFilePath set; skipped for prisma-next). 3. False 'skills loaded'. The Claude/Codex handoff launch prompt referenced the skills dir unconditionally; a stripped build installs no skills, so it told the agent to read files that aren't there. The clause is now conditional on installSkills() actually copying something. Tests: install-deps skew tests updated to assert the refusal (still never mutates); new init-command honest-summary tests (EQL-missing → exit 1 + 'Setup incomplete'; prisma-next eqlInstalled=false → completes). Skill + doc comments updated. Suite 542 unit / 62 e2e green, code:check clean. Changeset: stash minor. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
…ss (rc.2 M4) (#687) * feat(cli): honest non-interactive init — fail on skew, no false success (rc.2 M4) Non-interactive `stash init` reported success for setups that didn't fully complete. Three honesty gaps closed: 1. Version skew. A non-interactive run can't reconcile a `behind` skew (it won't mutate an install without consent), so instead of warning-and- proceeding — scaffolding against packages older than this CLI expects and then claiming success — it now REFUSES with a non-zero exit (CliExit(1)) and the exact align command. The #661/#666 no-mutation principle is preserved (execSync still not called); only the post-warning action changes from proceed to fail. `ahead` skew stays a warn (install likely fine; update the CLI). 2. False 'Setup complete'. The summary header and the auth/db/scaffold checkmarks were unconditional. Now: when EQL is required but not installed (eqlInstalled=false AND integration !== prisma-next), the summary reads 'Setup incomplete' and init exits non-zero, pointing at `stash eql install`. 'Database connection verified' → 'Database URL resolved' (init resolves a URL, never opens a connection). 'Encryption client scaffolded' is shown only when a client was written (clientFilePath set; skipped for prisma-next). 3. False 'skills loaded'. The Claude/Codex handoff launch prompt referenced the skills dir unconditionally; a stripped build installs no skills, so it told the agent to read files that aren't there. The clause is now conditional on installSkills() actually copying something. Tests: install-deps skew tests updated to assert the refusal (still never mutates); new init-command honest-summary tests (EQL-missing → exit 1 + 'Setup incomplete'; prisma-next eqlInstalled=false → completes). Skill + doc comments updated. Suite 542 unit / 62 e2e green, code:check clean. Changeset: stash minor. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w * fix(cli): honest init summary + skills guidance (review on #687) Two "only claim what actually happened" gaps found in review: - Copilot: the "✓ Encryption client scaffolded" checkmark was gated on `state.clientFilePath`, which build-schema sets even when it KEEPS an existing file (writes nothing). Gate on `state.schemaGenerated` (true only when a placeholder was written); show "✓ Encryption client kept (existing file)" for the keep path. Prisma Next (no clientFilePath) still shows neither. - CodeRabbit: the setup-prompt "Skills loaded" section pointed at skill directories and `AGENTS.md` regardless of what the handoff wrote — so a stripped build (no skills) sent the agent to read files that don't exist, and the Claude handoff (which never writes AGENTS.md) named it as the doctrine source. New `skillsLoadedLines(handoff, installedSkills)` renders the section honestly: no skills → point only at what was written (AGENTS.md for codex/agents-md, the docs for claude-code); claude-code → doctrine is in the installed skills, not AGENTS.md. Also hedged "list the skills loaded (if any)". Added setup-prompt honesty tests (empty-skills × handoff) and de-duped the two identical inline blocks. 557 tests green; build + biome clean. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w * fix(cli): honest Drizzle EQL outcome + close test gaps (review on #687) Differential review (Codex) found the completion gate still reported a successful, complete setup for `stash init --drizzle` before EQL was in the database: the Drizzle path only *generates* a v2 migration (applied later with `drizzle-kit migrate`), yet `installEqlStep` returned `eqlInstalled: true` for every non-throwing outcome — the exact false-success this PR removes elsewhere. - `installCommand` now returns a structured `InstallOutcome` (`installed` | `already-installed` | `migration-generated` | `dry-run`). - `installEqlStep` maps `migration-generated` to a new `eqlMigrationPending` state (EQL not in the DB yet) instead of `eqlInstalled`. - The init summary reports it honestly — "○ EQL migration generated — apply it with `drizzle-kit migrate`" — and excuses it from the "Setup incomplete" hard-fail (exit 0), the same treatment Prisma Next already gets. - Changeset + stash-cli skill updated to match. Test-gap review: added the previously-uncovered branches — - init summary's three-way encryption-client checkmark (scaffolded / kept); - the Drizzle migration-pending gate (cross-layer: outcome → step → summary); - handoff-claude / handoff-codex launch-prompt skills clause (stripped build drops the skills dir; codex keeps AGENTS.md). Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
Why
Part of #661 — the CLI-side fix.
stash initinstalled runtime packages unpinned, resolving through npm dist-tags, which lag or point at placeholders during a pre-release window. The 2026-07-16 skilltester run hit this end to end: a bare install delivered@cipherstash/stack@0.19.0+@cipherstash/stack-drizzle@0.0.0instead of rc.1 — broken/v3imports, and two eval surfaces silently testing the wrong release (where v3 scalarencryptQuerystill threw), forcing them onto EQL v2.What
src/runtime-versions.ts(new): the exact versions ofstash,@cipherstash/stack,-drizzle,-supabase, and@cipherstash/prisma-nextfrom this release train, embedded at build time via tsupdefine(same mechanism as the PostHog key; set on every build since it needs no env var). Versions are read from the sibling workspace manifests, so the embed can never disagree with what Changesets publishes together; a missing manifest fails the build instead of silently degrading to unpinned.stash initpins its installs to those versions (npm install @cipherstash/stack@1.0.0-rc.X …).node_modulesversion differs from the release's, init warns with both versions and prints the exact align command — the skew that made the eval agents test 0.19.0 was invisible behind a^-range spec.db validateetc.) and theinstallCommandwritten into.cipherstash/context.json(agents copy it).skills/stash-clidocuments the pinning + skew-warning behaviour. Changeset:stashpatch.Verification
runtime-versions+ pinning/skew cases); 57/57 pty E2E.npm install @cipherstash/stack@1.0.0-rc.1/npm install -D stash@1.0.0-rc.1(the versions it was built alongside — becomes rc.2 automatically at the release build).Not in this PR (registry side of #661, needs npm owner)
npm deprecate @cipherstash/stack-drizzle@0.0.0/@cipherstash/stack-supabase@0.0.0("placeholder — install @rc / 1.x").latestat the rc (they have no stable release, solatest=0.0.0 serves only broken installs).@cipherstash/stack@latestcan stay 0.19.0 until GA.https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
Summary by CodeRabbit
stash initnow installs the exact@cipherstash/*andstashversions shipped with the current CLI release.stash-cliinit documentation to reflect version pinning and the new version-skew warning behavior.Summary by CodeRabbit
stash initnow installs release-matched, pinned package versions.