fix(cli): unwrap default export when loading stash.config.ts#375
fix(cli): unwrap default export when loading stash.config.ts#375calvinbrewer merged 2 commits intomainfrom
Conversation
🦋 Changeset detectedLatest commit: 79f4a0b The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughFixes a bug in Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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. Review rate limit: 0/1 reviews remaining, refill in 60 minutes.Comment |
Closes #374. `loadStashConfig` was passing `interopDefault: true` to `createJiti(...)`, but in jiti 2.x the constructor option only applies to the deprecated synchronous `jiti(id)` callable form — the async `jiti.import()` ignores it and always returns the full module namespace. With `export default defineConfig({...})` that meant Zod was validating `{ default: { databaseUrl, client } }` and emitting databaseUrl: Invalid input: expected nonoptional, received undefined even though the user's config plainly set the field. The jiti 2.x async API exposes a per-call `{ default: true }` option that does work. Switch to it and drop the now-misleading constructor option from both `loadStashConfig` and `loadEncryptConfig`. `loadEncryptConfig` wasn't symptom-bugged (it iterates `Object.values` to find the EncryptionClient, which flattens both shapes equally) but keeping the two call sites consistent prevents the next reader from reasoning their way to the same wrong conclusion. Adds `config-jiti-integration.test.ts` — drives `loadStashConfig` against real jiti and a real temp `stash.config.ts`. The existing `config.test.ts` mocks `jiti.import` past the bug and so couldn't catch wrap/unwrap regressions on its own.
68d82f5 to
e3fe3cd
Compare
calvinbrewer
left a comment
There was a problem hiding this comment.
Validated by building the CLI locally and using the executable in an example application. Thanks @coderdan!
Onboarding users running `stash db install` (or any of the six other
DB-touching commands) without an exported `DATABASE_URL` would hit a
cryptic "Invalid stash.config.ts: databaseUrl received undefined"
error from Zod. The CLI already loads `.env.local` / `.env`* via
dotenv, but had no story for `--database-url` flags, local Supabase,
or a pasted-once one-off value.
Adds `src/config/database-url.ts` exporting `resolveDatabaseUrl()` —
called at the top of every DB-touching command before
`loadStashConfig`. It walks sources in order:
1. `--database-url <url>` flag (explicit override)
2. `process.env.DATABASE_URL` (shell, mise, direnv, dotenv files)
3. `supabase status --output env` -> DB_URL, gated on `--supabase`
or a `supabase/config.toml` in cwd
4. Interactive `p.text` prompt (skipped under `CI=true` or
non-TTY stdin)
5. Hard fail with a source-naming error message
The resolved URL is mutated into `process.env.DATABASE_URL` (only
when env was unset/empty, or the source was an explicit flag) so the
existing scaffolded `process.env.DATABASE_URL!` reference in
`stash.config.ts` resolves transparently. The connection string is
never written to disk — `stash.config.ts` keeps its declarative env
reference. The source label is logged (`Using DATABASE_URL from
--database-url flag` / `from supabase status` / `from prompt`) but
the URL itself is never echoed.
Wired through `bin/stash.ts` argv (the parser already supports
`--key value`), HELP text updated. All seven DB-touching commands
(`db install`, `db push`, `db upgrade`, `db status`, `db validate`,
`db test-connection`, `schema build`) accept `--database-url` and
call the resolver before `loadStashConfig`.
After a prompt-sourced run, a `p.note` nudges the user to set
DATABASE_URL in the existing dotenv file (or `.env` if none exists)
so they don't get re-prompted next time.
Tests:
- 13 unit tests (`__tests__/database-url.test.ts`) covering each
source, malformed flag, empty-string-env fallthrough, supabase
fallthrough on missing binary / no-DB_URL output / no project,
prompt-cancel, CI guard, non-TTY guard, hint-file detection.
- 2 E2E tests (`tests/e2e/database-url.e2e.test.ts`) driving the
built binary through the pty harness: `--database-url` flag
surfaces the source label and proceeds to (failing) connection,
CI=true with no creds exits 1 with the CI message.
`init`/build-schema is intentionally NOT wired through the resolver:
it reads `process.env.DATABASE_URL` for provider hints pre-config and
should not trigger the prompt flow.
Note: stacks on top of #375 (the jiti default-export unwrap fix). The
resolver populates env, but `loadStashConfig` still needs the jiti
fix to read the resulting config correctly. Both PRs together
deliver the full onboarding experience.
Closes #374.
Symptom
Even when the user's
stash.config.tsclearly setsdatabaseUrlfromprocess.env.DATABASE_URL, and aconsole.logof the same env var prints the URL just before the error fires.Root cause
loadStashConfig(packages/cli/src/config/index.ts) was constructing jiti withinteropDefault: trueand then callingjiti.import(...):In jiti 2.x, the constructor's
interopDefaultonly applies to the deprecated synchronousjiti(id)callable form. The asyncjiti.import()ignores it and always returns the module namespace. So forexport default defineConfig({...}),rawConfigis{ default: { databaseUrl, client } }. Zod then validates the wrapper, finds no top-leveldatabaseUrl, and emits the misleading "expected nonoptional, received undefined" error.Verified empirically against
jiti@2.6.1:Fix
Switch to the per-call
{ default: true }option (the jiti 2.x async-API way to ask for default-export unwrapping). Drop the now-misleadinginteropDefault: truefrom bothloadStashConfigand the symmetricloadEncryptConfigcall site (which wasn't symptom-bugged because it iteratesObject.valuesto find the EncryptionClient — but consistency keeps the next reader from reaching the same wrong conclusion).Why the existing test missed it
packages/cli/src/__tests__/config.test.tsmocksjiti.importto return the already-unwrapped shape:So the test never exercised the real jiti behavior and would pass regardless of which option was set.
Regression test
New
src/__tests__/config-jiti-integration.test.tsdrivesloadStashConfigagainst real jiti and a realstash.config.tsfile written into a temp dir. Two cases:export default {...}is unwrapped to the inner config (the regression we're fixing).databaseUrlproduces a useful error message containingInvalid stash.config.tsanddatabaseUrl.The mocked
config.test.tsstays in place for fast schema iteration.Test plan
pnpm --filter @cipherstash/cli buildcleanpnpm --filter @cipherstash/cli test— 78 pass (was 76; +2 from the new integration test)biome checkclean on changed filesnpx @cipherstash/cli db installagainst a config that readsdatabaseUrlfromprocess.env.DATABASE_URL) succeeds with this branchSummary by CodeRabbit
Bug Fixes
Fixed configuration validation to correctly process default exports from
stash.config.ts. Configuration files will now be reliably validated and recognized by the CLI, ensuring your settings are properly applied.Added integration tests to verify configuration file loading behavior and prevent regressions in future updates.