Skip to content

fix(cli): actionable config-scaffold errors instead of dead-ends#580

Merged
coderdan merged 10 commits into
mainfrom
fix/cli-config-scaffold-dx
Jul 8, 2026
Merged

fix(cli): actionable config-scaffold errors instead of dead-ends#580
coderdan merged 10 commits into
mainfrom
fix/cli-config-scaffold-dx

Conversation

@coderdan

@coderdan coderdan commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Fixes two config-scaffold dead-ends reported from testing the 0.17 CLI.

Closes #578
Closes #579

Issue 1 — commands dead-end on a missing config (#578)

eql status (and any command that loads the encryption client) in a project without a stash.config.ts printed a hand-write-it message and cancelled — it never pointed at the commands that set it up.

Fix: the missing-config message now recommends stash init (full setup) / stash eql install (scaffolds a config) — runner-aware (npx / pnpm dlx / …) — before the hand-written fallback.

Issue 2 — eql install doesn't actually need a config (#579)

The original crash:

◆  Created stash.config.ts
Error: Failed to load stash.config.ts …
Error: Cannot find module 'stash'

eql install scaffolded a stash.config.ts (which imports stash) and immediately loaded it — but stash / @cipherstash/stack are only project dependencies after stash init. Run standalone via npx, they're in the npx cache, not the project, so jiti couldn't resolve the config's import.

Fix (the real root cause): eql install only needs a database URL — it reads nothing else off the config that isn't otherwise available. So it now resolves the URL directly (--database-urlDATABASE_URLsupabase status → prompt) via resolveDatabaseUrl, instead of requiring a config:

  • An existing stash.config.ts is still authoritative (later workflow commands — db push, schema build, encrypt * — load the client through it), so it's loaded when present.
  • Without one, the URL is resolved directly and a config is offered as a convenience for the rest of the workflow (auto-created in non-interactive contexts), never required.
  • A standalone npx stash eql install --database-url <url> now works in a bare project with zero dependencies — there's no import 'stash' to resolve, so the crash can't happen.
  • An explicit --database-url is treated as a one-shot install: it leaves the project untouched (no stash.config.ts or client scaffolded). Without the flag, a config is offered for the rest of the workflow.

As a safety net, loadStashConfig still translates a missing-module load failure (a project that has a config but lacks the CLI packages) into actionable guidance for every command, rather than a jiti/Node stack trace.

$ npx stash eql install --database-url postgres://…   # bare project, no deps
●  Using DATABASE_URL from --database-url flag
◆  Created stash.config.ts
◆  Scaffolded encryption client at ./src/encryption/index.ts
…  (proceeds straight to installing EQL — no "Cannot find module 'stash'")

Design note: this replaces an earlier commit that guarded the scaffolded config's dependencies. Per review discussion, decoupling eql install from the config is the cleaner root fix, so the guard was removed.

Why no test caught it

db/config-scaffold.ts had zero tests, and the install-command tests run inside the monorepo where stash always resolves via the workspace self-reference — so the npx-standalone failure mode was never exercised. (That same quirk means the loadStashConfig catch can't be reproduced end-to-end in-repo; it's covered by a mocked-jiti unit test, and the e2e file documents why.)

Tests

All unit + e2e pass; Biome clean. Changeset: stash patch.

Summary by CodeRabbit

  • Bug Fixes
    • Improved CLI guidance when stash.config.ts is missing or can’t be loaded, replacing raw module-resolution noise with actionable next steps (stash init / stash eql install) tailored to the package manager.
    • stash eql install no longer depends on stash.config.ts; it resolves databaseUrl from --database-url → env → Supabase status → prompt, honoring an existing config for later workflow steps.
    • Supplying --database-url now runs as a one-shot install that does not scaffold files.
    • Non-interactive runs won’t create or overwrite configs.
  • Documentation
    • Updated --database-url help text to reflect the resolution order and one-shot behavior.
  • Tests
    • Expanded unit and e2e coverage for scaffolding, one-shot behavior, and improved missing-dependency messaging.

Closes #581

@coderdan coderdan requested a review from a team as a code owner July 8, 2026 07:04
@changeset-bot

changeset-bot Bot commented Jul 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: edf4db2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
stash Patch
@cipherstash/e2e Patch

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

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR updates CLI error handling, config scaffolding, and eql install so database URLs can be resolved without requiring stash.config.ts, while missing-module failures now print actionable setup guidance.

Changes

Config guidance, scaffolding, and install flow

Layer / File(s) Summary
Module-resolution helpers
packages/cli/src/module-error.ts, packages/cli/src/native.ts, packages/cli/src/config/missing-package.ts, packages/cli/src/config/__tests__/missing-package.test.ts
Adds shared module-not-found parsing, reuses it in native binary detection, and translates missing stash / @cipherstash/stack imports into targeted install guidance.
Config load guidance
packages/cli/src/config/index.ts, packages/cli/src/__tests__/config.test.ts, .changeset/stash-cli-config-scaffold-dx.md
Updates missing-config and config-load failures to print runner-aware setup or install instructions, exports findConfigFile, and expands unit coverage for the new messages.
Config scaffold flow
packages/cli/src/commands/db/config-scaffold.ts, packages/cli/src/commands/db/__tests__/config-scaffold.test.ts
Replaces ensureStashConfig with offerStashConfig, adds a default client path, and changes scaffolding to support ensure, offer, or skip behavior.
Install context resolution
packages/cli/src/commands/db/install.ts, packages/cli/src/bin/main.ts, packages/cli/src/cli/registry.ts, packages/cli/src/config/database-url.ts
Adds explicit scaffold control, resolves install context from either config or direct URL lookup, and updates CLI help and resolution docs to describe the URL precedence chain.
Init and e2e coverage
packages/cli/src/commands/init/steps/install-eql.ts, packages/cli/src/commands/init/steps/__tests__/install-eql.test.ts, packages/cli/tests/e2e/config-scaffold.e2e.test.ts
Passes ensure-mode scaffolding from init and expands end-to-end coverage for missing-config, missing-URL, one-shot URL, and env-based install paths.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • cipherstash/stack#349: Also changes the CLI config-scaffolding and db install onboarding flow.
  • cipherstash/stack#357: Also touches packages/cli/src/commands/db/install.ts and the cs_migrations install path.
  • cipherstash/stack#543: Also changes the eql install routing and command wiring in packages/cli/src/bin/main.ts.

Suggested reviewers: auxesis, calvinbrewer

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: making config-scaffold errors actionable.
Linked Issues check ✅ Passed The PR satisfies #578, #579, and #581 by adding actionable missing-config guidance, decoupling eql install from config, and documenting URL precedence.
Out of Scope Changes check ✅ Passed The changes appear scoped to config scaffolding, install flow, and related error handling/tests, with no clear unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/cli-config-scaffold-dx

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.

@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: 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/commands/db/config-scaffold.ts`:
- Line 110: The CI check in the config scaffold command is duplicated and should
use the shared helper for consistency with resolveDatabaseUrl. Update the isTTY
logic in config-scaffold to call isCiEnv() instead of reading process.env.CI
directly, keeping the existing stdin TTY check intact and reusing the same CI
detection path used elsewhere.

In `@packages/cli/tests/e2e/config-scaffold.e2e.test.ts`:
- Around line 42-49: The e2e test in config-scaffold.e2e.test.ts is leaking
DATABASE_URL from the parent environment, so the `run` call can behave
nondeterministically. Update the `run(['eql', 'install'], ...)` setup to
explicitly unset or override DATABASE_URL in the child process environment,
while preserving required vars like PATH if `run` does not merge env by default.
Keep the existing assertions around `stash.config.ts` and the error output
intact.
🪄 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: 6e65dc0b-e0cb-47b1-937c-6df51433fb00

📥 Commits

Reviewing files that changed from the base of the PR and between 122fb0d and 0c9daae.

📒 Files selected for processing (6)
  • .changeset/stash-cli-config-scaffold-dx.md
  • packages/cli/src/commands/db/__tests__/config-scaffold.test.ts
  • packages/cli/src/commands/db/config-scaffold.ts
  • packages/cli/src/commands/db/install.ts
  • packages/cli/src/config/index.ts
  • packages/cli/tests/e2e/config-scaffold.e2e.test.ts
✅ Files skipped from review due to trivial changes (1)
  • .changeset/stash-cli-config-scaffold-dx.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/cli/src/config/index.ts

Comment thread packages/cli/src/commands/db/config-scaffold.ts Outdated
Comment thread packages/cli/tests/e2e/config-scaffold.e2e.test.ts Outdated

@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: 1

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/db/install.ts (1)

100-107: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Honor --database-url before loading/scaffolding project config.

Line 100 takes the existing config branch before the explicit one-shot flag check, so stash eql install --database-url ... can still load stash.config.ts and then scaffold a missing encryption client via Line 153. That contradicts the one-shot contract described in this PR.

Proposed fix
 async function resolveInstallContext(
   options: InstallOptions,
   s: ReturnType<typeof p.spinner>,
 ): Promise<{ databaseUrl: string; clientPath: string | null }> {
+  // One-shot install (explicit --database-url): don't touch the project.
+  if (options.databaseUrl) {
+    const databaseUrl = await resolveDatabaseUrl({
+      databaseUrlFlag: options.databaseUrl,
+      supabase: options.supabase,
+    })
+    return { databaseUrl, clientPath: null }
+  }
+
   if (findConfigFile(process.cwd())) {
     s.start('Loading stash.config.ts...')
     const config = await loadStashConfig({
       databaseUrlFlag: options.databaseUrl,
       supabase: options.supabase,
@@
-  // One-shot install (explicit --database-url): don't touch the project.
-  if (options.databaseUrl) {
-    return { databaseUrl, clientPath: null }
-  }
-
   const clientPath = await offerStashConfig()
   return { databaseUrl, clientPath }
 }

Also applies to: 153-155

🤖 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/db/install.ts` around lines 100 - 107, The install
flow in db/install.ts is checking findConfigFile() before honoring the one-shot
--database-url flag, which lets install still load stash.config.ts and scaffold
a client. Update the main command path and the client-scaffolding branch in
install() so the explicit options.databaseUrl/one-shot flow is handled first and
immediately returns without touching project config or generating the encryption
client. Use the existing symbols loadStashConfig, findConfigFile, and the
install command’s scaffold logic to keep the override behavior consistent.
🧹 Nitpick comments (1)
packages/cli/tests/e2e/config-scaffold.e2e.test.ts (1)

52-73: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add exit code assertion and a test-level timeout.

The test verifies no files are scaffolded but doesn't assert the command actually failed (r.exitCode). Adding an exit code check strengthens the "one-shot fails cleanly" intent. A vitest timeout (e.g., 15s) is also a prudent safety net — connect_timeout=2 bounds the DB connection, but other phases could theoretically hang.

♻️ Proposed additions
 it('`eql install --database-url` is one-shot — it never scaffolds project files', async () => {
   // An explicit --database-url means "install EQL against this DB now"; it
   // must not drop a stash.config.ts or an encryption client into the project.
   // The bogus URL fails fast at connect (past the config stage), which is all
   // we need to observe the no-scaffold behaviour.
   const r = await run(
     [
       'eql',
       'install',
       '--database-url',
       'postgres://u:p@127.0.0.1:1/db?connect_timeout=2',
     ],
     { cwd: tmpDir },
   )

+  expect(r.exitCode).not.toBe(0)
   expect(fs.existsSync(path.join(tmpDir, 'stash.config.ts'))).toBe(false)
   expect(
     fs.existsSync(path.join(tmpDir, 'src', 'encryption', 'index.ts')),
   ).toBe(false)
   expect(r.output).not.toContain('Created stash.config.ts')
   expect(r.output).not.toContain('Cannot find module')
-}, 15_000)
+})

As per coding guidelines, e2e tests in tests/e2e/**.e2e.test.ts should cover exit codes for top-level commands.

🤖 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/tests/e2e/config-scaffold.e2e.test.ts` around lines 52 - 73, The
e2e test for the `eql install --database-url` flow should also verify the
command failed and won’t hang. Update the `it(...)` case in
`config-scaffold.e2e.test.ts` to assert `r.exitCode` is non-zero, and add a
test-level timeout to this spec so the top-level `run(...)` invocation in the
`eql install` scenario is bounded even if later phases stall.

Source: Coding guidelines

🤖 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/db/install.ts`:
- Line 120: The direct-install dry-run path is mutating the project before the
dry-run guard runs, because `offerStashConfig()` and `ensureEncryptionClient()`
can write scaffold/client files in `install.ts` even when no changes should be
made. Move the dry-run handling earlier in the flow so the `db install` command
short-circuits before any file-writing work, and make sure the
`offerStashConfig()` and `ensureEncryptionClient()` calls are only reached when
the command is not running in dry-run mode. Keep the existing dry-run message
behavior, but ensure it is reached without creating `stash.config.ts` or the
client file.

---

Outside diff comments:
In `@packages/cli/src/commands/db/install.ts`:
- Around line 100-107: The install flow in db/install.ts is checking
findConfigFile() before honoring the one-shot --database-url flag, which lets
install still load stash.config.ts and scaffold a client. Update the main
command path and the client-scaffolding branch in install() so the explicit
options.databaseUrl/one-shot flow is handled first and immediately returns
without touching project config or generating the encryption client. Use the
existing symbols loadStashConfig, findConfigFile, and the install command’s
scaffold logic to keep the override behavior consistent.

---

Nitpick comments:
In `@packages/cli/tests/e2e/config-scaffold.e2e.test.ts`:
- Around line 52-73: The e2e test for the `eql install --database-url` flow
should also verify the command failed and won’t hang. Update the `it(...)` case
in `config-scaffold.e2e.test.ts` to assert `r.exitCode` is non-zero, and add a
test-level timeout to this spec so the top-level `run(...)` invocation in the
`eql install` scenario is bounded even if later phases stall.
🪄 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: 8f188809-a948-4d2b-b685-3be5f8d56507

📥 Commits

Reviewing files that changed from the base of the PR and between 0c9daae and fbe854b.

📒 Files selected for processing (3)
  • .changeset/stash-cli-config-scaffold-dx.md
  • packages/cli/src/commands/db/install.ts
  • packages/cli/tests/e2e/config-scaffold.e2e.test.ts
✅ Files skipped from review due to trivial changes (1)
  • .changeset/stash-cli-config-scaffold-dx.md

Comment thread packages/cli/src/commands/db/install.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the stash CLI’s config-scaffolding and database URL resolution flows to avoid “dead-end” errors, particularly for standalone usage (npx stash ...) in projects that don’t yet have stash.config.ts or local dependencies installed.

Changes:

  • Make missing-config and missing-module failures produce runner-aware, actionable guidance (stash init / stash eql install) instead of dead-end instructions or raw loader errors.
  • Decouple stash eql install from requiring stash.config.ts by resolving DATABASE_URL directly when no config exists, and treating --database-url as a one-shot “don’t scaffold” signal.
  • Add unit + E2E coverage for the new scaffold/loader behaviors and updated help text.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/cli/src/config/index.ts Improves missing-config messaging and translates missing-module loader failures into actionable install/init guidance.
packages/cli/src/config/database-url.ts Clarifies resolver documentation; resolver remains the single source of truth for URL resolution priority.
packages/cli/src/commands/db/install.ts Changes eql install to resolve DB URL without requiring config; respects one-shot --database-url behavior.
packages/cli/src/commands/db/config-scaffold.ts Makes config creation optional/offer-based and supports non-interactive scaffolding paths.
packages/cli/src/commands/db/tests/config-scaffold.test.ts Adds unit tests for offerStashConfig behavior (non-interactive create, detection, no overwrite).
packages/cli/src/tests/config.test.ts Adds unit tests for actionable missing-config and missing-module guidance behavior.
packages/cli/tests/e2e/config-scaffold.e2e.test.ts Adds E2E coverage in a temp project outside the monorepo to catch standalone npx failure modes.
packages/cli/src/cli/registry.ts Updates shared --database-url flag description to document resolution precedence.
packages/cli/src/bin/main.ts Updates --help output to describe --database-url resolution precedence.
.changeset/stash-cli-config-scaffold-dx.md Adds a patch changeset documenting the user-facing CLI DX fixes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/cli/src/commands/db/config-scaffold.ts Outdated
Comment thread packages/cli/src/cli/registry.ts

@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: 1

♻️ Duplicate comments (2)
packages/cli/src/commands/db/install.ts (1)

107-133: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

resolveInstallContext can write scaffold files before the dry-run guard.

resolveInstallContext calls offerStashConfig (line 131) which can write stash.config.ts — in 'ensure' mode unconditionally, in 'offer' mode when TTY and user accepts. Neither path checks options.dryRun. If the dry-run guard in installCommand is still downstream of this call, eql install --dry-run can modify the project before printing "no changes will be made."

This was flagged in a prior review and does not appear to have been addressed. Verify the guard location and add an early return if needed:

🛡️ Proposed fix
   const mode = options.scaffoldConfig ?? 'offer'
   if (mode === 'skip') {
     return { databaseUrl, clientPath: null }
   }
 
+  if (options.dryRun) {
+    return { databaseUrl, clientPath: null }
+  }
+
   const clientPath = await offerStashConfig({ ensure: mode === 'ensure' })
   return { databaseUrl, clientPath }

Run the following script to verify the dry-run guard location in installCommand:

#!/bin/bash
# Description: Check where the dry-run guard sits relative to resolveInstallContext
# and ensureEncryptionClient calls in installCommand.

# Map the file structure
ast-grep outline packages/cli/src/commands/db/install.ts --items all --type function

# Then read the installCommand function body to find the dry-run guard
sed -n '135,230p' packages/cli/src/commands/db/install.ts
🤖 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/db/install.ts` around lines 107 - 133,
`resolveInstallContext` may still create or scaffold `stash.config.ts` before
the dry-run check runs, so `installCommand` can mutate the project during
`--dry-run`. Move the dry-run guard so it happens before calling
`resolveInstallContext`, or add an early return inside `resolveInstallContext`
when `options.dryRun` is set, and make sure the `offerStashConfig` path is never
reached in dry-run mode. Use `installCommand` and `resolveInstallContext` as the
main points to verify the guard placement.
packages/cli/tests/e2e/config-scaffold.e2e.test.ts (1)

42-49: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Isolate DATABASE_URL from the parent environment.

run(['eql', 'install'], { cwd: tmpDir }) doesn't pass env, so the child process inherits the parent environment. If DATABASE_URL is set in CI or the local shell, the CLI resolves a URL and proceeds past the "Cannot resolve DATABASE_URL" message, causing this test to fail nondeterministically.

This was flagged in a prior review and remains unaddressed.

🛡️ Proposed fix
-    const r = await run(['eql', 'install'], { cwd: tmpDir })
+    const r = await run(['eql', 'install'], {
+      cwd: tmpDir,
+      env: { ...process.env, DATABASE_URL: '' },
+    })

If run merges rather than replaces env, adjust accordingly to ensure DATABASE_URL is unset while preserving required vars.

🤖 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/tests/e2e/config-scaffold.e2e.test.ts` around lines 42 - 49, The
e2e test for config scaffolding is inheriting the parent shell’s DATABASE_URL,
making `run(['eql', 'install'], { cwd: tmpDir })` nondeterministic. Update the
`run` invocation in `config-scaffold.e2e.test.ts` to explicitly isolate the
child environment by unsetting DATABASE_URL while preserving any required
variables, so the `stash.config.ts` failure path is exercised consistently. Use
the `run` helper call in this test as the fix point and keep the existing
assertions against the unresolved DATABASE_URL error.
🤖 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/tests/e2e/config-scaffold.e2e.test.ts`:
- Around line 75-92: The non-interactive `eql install` e2e test is accidentally
relying on an inherited `DATABASE_URL`, so it may not cover the missing-env path
reliably. Update the `config-scaffold.e2e.test.ts` case that calls `run(['eql',
'install'], ...)` to explicitly clear `DATABASE_URL` in the test env, keeping
the existing assertions that no `stash.config.ts` or client scaffold is written
and that no module import errors appear.

---

Duplicate comments:
In `@packages/cli/src/commands/db/install.ts`:
- Around line 107-133: `resolveInstallContext` may still create or scaffold
`stash.config.ts` before the dry-run check runs, so `installCommand` can mutate
the project during `--dry-run`. Move the dry-run guard so it happens before
calling `resolveInstallContext`, or add an early return inside
`resolveInstallContext` when `options.dryRun` is set, and make sure the
`offerStashConfig` path is never reached in dry-run mode. Use `installCommand`
and `resolveInstallContext` as the main points to verify the guard placement.

In `@packages/cli/tests/e2e/config-scaffold.e2e.test.ts`:
- Around line 42-49: The e2e test for config scaffolding is inheriting the
parent shell’s DATABASE_URL, making `run(['eql', 'install'], { cwd: tmpDir })`
nondeterministic. Update the `run` invocation in `config-scaffold.e2e.test.ts`
to explicitly isolate the child environment by unsetting DATABASE_URL while
preserving any required variables, so the `stash.config.ts` failure path is
exercised consistently. Use the `run` helper call in this test as the fix point
and keep the existing assertions against the unresolved DATABASE_URL error.
🪄 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: 605f8d38-d0ae-478b-9784-4aa229e36539

📥 Commits

Reviewing files that changed from the base of the PR and between fbe854b and 2dd3bba.

📒 Files selected for processing (9)
  • packages/cli/src/bin/main.ts
  • packages/cli/src/cli/registry.ts
  • packages/cli/src/commands/db/__tests__/config-scaffold.test.ts
  • packages/cli/src/commands/db/config-scaffold.ts
  • packages/cli/src/commands/db/install.ts
  • packages/cli/src/commands/init/steps/__tests__/install-eql.test.ts
  • packages/cli/src/commands/init/steps/install-eql.ts
  • packages/cli/src/config/database-url.ts
  • packages/cli/tests/e2e/config-scaffold.e2e.test.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/cli/src/config/database-url.ts

Comment thread packages/cli/tests/e2e/config-scaffold.e2e.test.ts
coderdan added 9 commits July 8, 2026 19:40
#579)

Two related config-scaffold failures reported from testing the 0.17 CLI:

- `eql status` (any read command) in a project with no stash.config.ts
  printed a "hand-write this file" message and exited — no scaffold, no
  pointer to the commands that set it up (#578).
- `npx stash eql install` scaffolded a stash.config.ts, then immediately
  crashed with a raw `Cannot find module 'stash'`: the config imports
  `stash` (and the client imports `@cipherstash/stack`), which resolve only
  once those are project dependencies — something only `stash init` did.
  Run standalone via npx, they live in the npx cache, not the project (#579).

Fixes:
- Missing-config message now recommends `stash init` / `stash eql install`
  (runner-aware) before the hand-written fallback.
- `eql install` checks for the config's dependencies after scaffolding and
  offers to install the missing ones (or prints the exact commands in
  non-interactive contexts) before jiti loads the config.
- `loadStashConfig` translates a missing-module load failure into the same
  actionable guidance for every command, instead of a jiti/Node stack trace.

Tests: config-scaffold.ts had zero tests and the install-command tests run
inside the monorepo where `stash` always resolves via the workspace — which
is why the crash escaped CI. Adds unit coverage for the dependency guard and
the load-error translation (mocked jiti), plus e2e coverage in a throwaway
temp project for the missing-config and missing-deps paths.
Follow-up to the previous commit: rather than guarding the scaffolded
config's dependencies, remove the requirement entirely. `eql install` only
needs a database URL, so it now resolves one directly (`--database-url` →
env → supabase → prompt) via resolveDatabaseUrl instead of scaffolding a
config and loading it.

- An existing stash.config.ts is still authoritative (later workflow commands
  load the encryption client through it) — load it when present.
- Without one, resolve the URL directly and *offer* to scaffold a config for
  the rest of the workflow (auto-creates it in non-interactive contexts) — it's
  a convenience, never a prerequisite. A standalone
  `npx stash eql install --database-url ...` now works in a bare project with
  zero deps: there's no `import 'stash'` to resolve, so the
  `Cannot find module 'stash'` crash can't happen.
- Drop the now-redundant dependency guard (ensureConfigDependencies /
  missingConfigDependencies) and revert the isPackageInstalled cwd param.
- Keep the improved messages: the actionable missing-config error (#578) and
  loadStashConfig's missing-module translation (#579) still cover the commands
  that genuinely load a config.

Replaces the guard unit tests with offerStashConfig coverage; the e2e now
asserts `eql install` resolves the URL directly and fails cleanly (no config
written, no module crash) when no URL is available.
…olding)

An explicit --database-url signals "install EQL against this DB now", so
resolveInstallContext no longer scaffolds project files in that case — neither
stash.config.ts nor the encryption client. Without the flag (URL from env /
supabase / prompt) the config is still offered/created for the rest of the
workflow. An existing config is honoured either way.
Make the precedence discoverable in three places:
- `resolveDatabaseUrl` gets a function-level JSDoc with the authoritative
  order (flag → DATABASE_URL env → supabase status → prompt → fail), plus the
  nuance that stash.config.ts is a container, not a separate tier — its default
  `databaseUrl: await resolveDatabaseUrl()` re-runs the same chain, so the flag
  wins even with a config present (except a hand-edited literal, which bypasses
  the resolver). The module header's duplicated list now points here.
- The registry `--database-url` descriptor (feeds `stash manifest`).
- The `stash --help` banner's DB / EQL Flags entry.
The one-shot rule was keyed on `options.databaseUrl` being set, but that field
is populated by two callers meaning different things: the CLI sets it only when
`--database-url` is passed (a genuine one-shot), while `stash init` always sets
it (a resolved URL, just to avoid re-prompting). So init tripped the one-shot
branch and never scaffolded stash.config.ts — leaving every downstream command
to dead-end on "Could not find stash.config.ts".

Make the scaffold intent explicit and caller-owned instead of inferred:

- InstallOptions gains `scaffoldConfig: 'ensure' | 'offer' | 'skip'`.
- runInstall (CLI) passes 'skip' when --database-url is present, else 'offer'.
- init's install-eql passes 'ensure' (create without a yes/no prompt).
- resolveInstallContext switches on scaffoldConfig, not options.databaseUrl.
- offerStashConfig gains an `ensure` mode; also folds the duplicated
  writeFileSync+log into a `writeStashConfig` helper.

Tests: new install-eql.test.ts asserts init requests scaffoldConfig: 'ensure'
(the exact wiring that regressed), plus an ensure-mode scaffold test. Fixed the
stale install-eql comment (installCommand now scaffolds but no longer loads the
config during init).
…#5)

offerStashConfig returned a truthy DEFAULT_CLIENT_PATH even when it created
nothing, so installCommand's `if (clientPath)` guard scaffolded the encryption
client anyway. Three related fixes, all on this one surface:

- #2: return null when no config is created (declined / cancelled / existing),
  so the caller skips the client scaffold too. Declining now touches nothing.
- #4: in 'offer' mode a non-interactive run no longer silently writes a config
  (+ client) into a bare project — those files import stash / @cipherstash/stack
  and would just re-defer the MODULE_NOT_FOUND crash to the next command. It
  returns null; the missing-config guidance points the user at `stash init`.
  ('ensure'/init still creates unconditionally; init installs the deps.)
- #5: use the shared isCiEnv() (handles CI=1 / CI=TRUE) instead of the raw
  `process.env.CI !== 'true'`, matching the sibling resolveDatabaseUrl so the
  two steps classify the same environment consistently.

Tests: offerStashConfig now asserts null + no write for the non-interactive
offer and existing-config cases; a new e2e drives `eql install` with the URL
from env (non-TTY) and asserts neither a config nor a client is written.
… missing (review #3, #6)

The missing-package translation only wrapped the CONFIG load, which imports bare
`stash` — so its `@cipherstash/stack` branch never fired. The package actually
fails to resolve in the CLIENT file (imported by loadEncryptConfig, used by
db push / schema build / encrypt), whose catch dumped a raw jiti stack trace.

- #3: apply the translation to loadEncryptConfig too, so a missing
  @cipherstash/stack (incl. subpaths like @cipherstash/stack/schema) yields the
  same "install it / run stash init" guidance for every command, not a trace.
- #6: replace the brittle substring matching (includes("'stash'")) with a shared,
  subpath-aware helper. New src/module-error.ts holds the primitives
  (isModuleNotFound + moduleNotFoundSpecifier's `Cannot find (module|package)`
  regex); native.ts now reuses them instead of its own copy, and a new
  config/missing-package.ts reduces a specifier to its package name before
  matching (so @cipherstash/stack/schema resolves to @cipherstash/stack).

Tests: unit coverage for missingCipherStashPackage (subpath, non-matches, code
gate) and a loadEncryptConfig test asserting the client-load path translates a
missing @cipherstash/stack into guidance rather than a raw trace.
… walk (#7, #8, #10)

- #7: collapse three copies of the default encryption-client path
  (DEFAULT_ENCRYPT_CLIENT_PATH in config/index.ts, DEFAULT_CLIENT_PATH in
  config-scaffold.ts, and a private copy in build-schema.ts) into one exported
  DEFAULT_CLIENT_PATH in config/index.ts, imported everywhere — so the scaffold
  default and the Zod default can't drift.
- #8: move the "`<pkg>` is not installed" guidance into messages.ts
  (db.missingCipherStashPackage) per the assertion-stable-strings convention.
- #10: loadStashConfig accepts an optional knownConfigPath; resolveInstallContext
  passes the path it already located so the cwd→root filesystem walk runs once
  instead of twice on the existing-config path.

No behaviour change; message output is byte-identical. Full unit + e2e green.
…, doc caveat

- Dry run no longer mutates the project (CodeRabbit): `eql install --dry-run`
  reached offerStashConfig()/ensureEncryptionClient() before the dry-run guard,
  so a config or client file could be written despite "no changes will be made".
  resolveInstallContext now skips scaffolding under dryRun, and the client
  scaffold is guarded on `!options.dryRun` (an existing config still yields a
  clientPath). Adds an e2e that asserts no client file is written on a dry run.
- Isolate DATABASE_URL in the "resolves URL directly" e2e (CodeRabbit): pass
  `env: { DATABASE_URL: '' }` so a value in the parent env can't leak in and
  resolve past the guidance the test asserts on.
- Document the literal-databaseUrl exception in the --database-url help
  (Copilot): a hand-set literal `databaseUrl` in stash.config.ts bypasses the
  resolver and wins over the flag/env/etc.
@coderdan coderdan force-pushed the fix/cli-config-scaffold-dx branch from cbb3079 to 7877bb4 Compare July 8, 2026 09:45

@freshtonic freshtonic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed with a multi-angle pass (correctness, removed-behavior, cross-file, reuse/simplification/efficiency/altitude), with each candidate independently verified against the head commit. The decoupling of eql install from the config is the right root-cause fix, tests are well-aimed at the actual failure modes, and several plausible-sounding concerns were checked and refuted (jiti 2.7.0 rethrows Node's coded module error unwrapped, so the MODULE_NOT_FOUND translation is sound; subpath skew on @cipherstash/stack throws ERR_PACKAGE_PATH_NOT_EXPORTED, which correctly falls through to the raw error). Four findings survived verification, most-severe first:

1. packages/cli/src/commands/db/install.ts:111--database-url is silently ignored when a parent-directory config hard-codes a literal databaseUrl (CONFIRMED, narrow trigger)

resolveInstallContext uses findConfigFile(process.cwd()), which walks up parent directories. Run eql install --database-url postgres://staging... from a monorepo subdirectory whose parent has a hand-edited config with a literal databaseUrl: 'postgres://prod...', and the literal wins — the resolver (and the flag threaded through withResolverContext) is never invoked, so EQL installs against the parent config's database with no warning. This is a behavior change: the old ensureStashConfig checked cwd only and would scaffold a fresh cwd config (whose resolveDatabaseUrl() honours the flag). The trigger is narrow (scaffolded configs never emit a literal), but the failure is "installed into the wrong database, silently." Consider logging which config was loaded from a parent directory, or warning when --database-url was passed but didn't determine the final URL.

2. packages/cli/src/commands/db/install.ts:174 — one-shot --database-url still scaffolds the client file when a config exists (PLAUSIBLE — docs/behavior mismatch)

The changeset promises an explicit --database-url run "leaves the project untouched (no config or client scaffolded)". But when a stash.config.ts already exists and points at a not-yet-created client, resolveInstallContext takes the config branch — which never consults scaffoldConfig: 'skip' — and returns a non-null clientPath, so ensureEncryptionClient writes src/encryption/index.ts into the project. The comment at install.ts:170-173 suggests this is deliberate, but then the changeset/PR wording overstates the guarantee. Either honour 'skip' in the config branch too, or scope the "untouched" claim to bare projects (and ideally add a non-dry-run test for the existing-config + --database-url case — the suite only covers it under --dry-run).

3. packages/cli/src/config/database-url.ts:153-160 — orphaned duplicate JSDoc on resolveDatabaseUrl

The new comprehensive doc block was added below the old one, so two adjacent /** */ blocks now document the same function; only the second attaches to the symbol. Delete the old block (its content is subsumed).

4. packages/cli/src/commands/db/config-scaffold.ts:136 — interactivity gate re-derived inline

Boolean(process.stdin.isTTY) && !isCiEnv() duplicates the identical isInteractive computation in config/database-url.ts:223 (and similar sites elsewhere). config/tty.ts exists precisely so every gate checks the same way — worth extracting an isInteractive() helper there so offerStashConfig can't diverge from the URL resolver it runs alongside. Minor.

Refuted during verification (for the record): --database-url=<url> equals-form parsing gap (pre-existing parser limitation, unchanged by this PR); missing-package misdiagnosis on subpath version skew (unreachable given the exports map); jiti wrapping module errors and defeating the translation (jiti 2.7.0 propagates the raw coded error).

@freshtonic freshtonic self-requested a review July 8, 2026 11:13

@freshtonic freshtonic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Provided some feedback - I'll leave it to you @coderdan to decide whether to fix in this this PR or in a follow-up.

Approved.

…config

Four findings from @freshtonic's review:

1. `--database-url` was silently overridden by a parent-directory config with a
   literal `databaseUrl` (findConfigFile walks up). A one-shot install
   (scaffoldConfig 'skip') now bypasses config loading entirely, so the flag
   always wins and no parent config can redirect the install. For the other
   modes, warn when a config's literal databaseUrl overrides the flag.

2. One-shot `--database-url` no longer scaffolds the client file when a config
   already exists — the config branch is skipped, so the project is truly left
   untouched. Added a non-dry-run e2e covering existing-config + --database-url.

3. Removed the orphaned duplicate JSDoc block on resolveDatabaseUrl.

4. Extracted shared isInteractive() into config/tty.ts and routed the
   DATABASE_URL resolver, config scaffolder, and Supabase install-mode gate
   through it instead of re-deriving `isTTY && !CI` inline.
@coderdan

coderdan commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @freshtonic — addressed all four in edf4db2.

1. --database-url silently overridden by a parent-dir literal config — fixed at the root. A one-shot install (--database-url alone → scaffoldConfig: 'skip') now bypasses config loading entirely in resolveInstallContext, so the flag always wins and no parent-directory config can redirect the install. For the non-one-shot modes (init passes a URL and 'ensure'), I added a p.log.warn when a config's literal databaseUrl differs from the flag, so it's never silently ignored.

2. One-shot --database-url still scaffolded the client with an existing config — fixed by the same bypass: the config branch is skipped, so no client file is written and the project is genuinely left untouched. Added the non-dry-run e2e you asked for (existing stash.config.ts + --database-url → asserts src/encryption/index.ts is not created). The changeset "leaves the project untouched" wording is now accurate rather than aspirational.

3. Orphaned duplicate JSDoc on resolveDatabaseUrl — deleted the old block.

4. Interactivity gate re-derived inline — extracted isInteractive() into config/tty.ts and routed the DATABASE_URL resolver, the config scaffolder, and the Supabase install-mode gate (which had its own isTTY && CI !== 'true') through it.

Full suites green: 406 unit + 55 e2e (was 54), Biome + tsc clean on the changed files.

@coderdan coderdan merged commit eeb704f into main Jul 8, 2026
9 checks passed
@coderdan coderdan deleted the fix/cli-config-scaffold-dx branch July 8, 2026 11: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

3 participants