Skip to content

feat(cli): layered DATABASE_URL resolution for db / schema commands#377

Open
coderdan wants to merge 1 commit intomainfrom
dan/database-url-resolution
Open

feat(cli): layered DATABASE_URL resolution for db / schema commands#377
coderdan wants to merge 1 commit intomainfrom
dan/database-url-resolution

Conversation

@coderdan
Copy link
Copy Markdown
Contributor

@coderdan coderdan commented Apr 30, 2026

Closes #376. Stacks on top of #375 (jiti default-export unwrap fix) — both together deliver the full onboarding experience.

Problem

Running any DB-touching command without DATABASE_URL exported in the shell or in a loaded .env* file fails with the cryptic Zod error:

Error: Invalid stash.config.ts
  - databaseUrl: Invalid input: expected nonoptional, received undefined

Users provide DB URLs through many channels: --database-url flag, shell exports, mise.toml, direnv, dotenv-cli, local Supabase, or simply "let me paste it once". The CLI handled only the dotenv path.

Fix

New resolver module src/config/database-url.ts exporting resolveDatabaseUrl(). Called at the top of every DB-touching command before loadStashConfig. Walks sources in order:

  1. --database-url <url> flag (explicit override)
  2. process.env.DATABASE_URL (shell, mise, direnv, dotenv files already loaded by bin/stash.ts)
  3. supabase status --output envDB_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 diskstash.config.ts keeps its declarative env reference. The source is logged (Using DATABASE_URL from --database-url flag / from supabase status / from prompt); the URL itself is never echoed.

After a prompt-sourced run, a p.note nudges the user to set DATABASE_URL in their existing dotenv file (or .env if none exists) so they don't get re-prompted.

Surface

--database-url <url> accepted on all seven DB-touching commands: db install, db push, db upgrade, db status, db validate, db test-connection, schema build. HELP text updated.

init/build-schema is not wired through the resolver — it reads process.env.DATABASE_URL for provider hints pre-config and should not trigger the prompt flow.

Test plan

  • pnpm --filter @cipherstash/cli test — 89 pass (was 76; +13 from new __tests__/database-url.test.ts)
  • pnpm --filter @cipherstash/cli test:e2e — 10 pass (was 8; +2 from new tests/e2e/database-url.e2e.test.ts: flag-source surfaces the label, CI guard exits 1 with the CI message)
  • biome check clean on changed files
  • pnpm --filter @cipherstash/cli build clean

Note on coverage

The unit tests use vi.mock to stub node:child_process, @clack/prompts, and detectSupabaseProject, so each source path is exercised in isolation. The E2E tests run the built dist/bin/stash.js through the pty harness against a temp stash.config.ts for the flag and CI-guard cases. The non-TTY path is covered by the unit suite (Object.defineProperty(process.stdin, 'isTTY', false)) since the pty harness always provides a TTY.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added --database-url CLI flag to database commands (db install, db push, db upgrade, db validate, db test-connection, and schema build) for explicit database URL specification.
  • Improvements

    • Database URL resolution now follows a layered precedence: explicit flag → environment variable → Supabase status → interactive prompt, with automatic CI environment detection to prevent prompts in non-interactive contexts.

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.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 30, 2026

⚠️ No Changeset found

Latest commit: d7bd47c

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

📝 Walkthrough

Walkthrough

Introduces a layered database URL resolver with a --database-url CLI flag and fallback chain (flag → environment → Supabase status → interactive prompt), integrated across all DB and schema commands with appropriate CI guards and user messaging.

Changes

Cohort / File(s) Summary
Core Database URL Resolver
packages/cli/src/config/database-url.ts
New resolveDatabaseUrl function implementing layered resolution across flag, environment, Supabase status output (when project detected), and interactive prompt. Validates URLs, populates process.env.DATABASE_URL, logs resolution source, and fails with code 1 on invalid/missing URL in CI or non-TTY environments; prompt cancellation exits with code 0.
CLI Entry Point
packages/cli/src/bin/stash.ts
Adds --database-url <url> flag documentation and threads databaseUrl parameter through all DB (install, upgrade, push, validate, test-connection, status) and schema (build) command invocations.
Database Commands
packages/cli/src/commands/db/install.ts, push.ts, status.ts, test-connection.ts, upgrade.ts, validate.ts
Updates function signatures to accept optional databaseUrl option and invokes resolveDatabaseUrl at command start before config loading, ensuring effective DATABASE_URL is available early in the execution flow.
Schema Commands
packages/cli/src/commands/schema/build.ts
Extends builderCommand to accept databaseUrl option and invokes resolveDatabaseUrl before schema building logic; includes minor formatting changes to schemas.map(...) expressions.
User Messages
packages/cli/src/messages.ts
Adds database URL resolution messages covering source labels (flag, Supabase, prompt), prompt guidance, validation errors, CI guard failures, and dotenv hint generator for post-prompt workflow.
Unit Tests
packages/cli/src/__tests__/database-url.test.ts
Comprehensive Vitest suite validating resolveDatabaseUrl behavior across all resolution paths (flag validation, env fallthrough, Supabase status parsing, interactive prompts), CI guards, and error conditions with mocked dependencies.
E2E Tests
packages/cli/tests/e2e/database-url.e2e.test.ts
End-to-end tests verifying flag-based resolution output and CI guard behavior through actual CLI invocation via PTY harness.

Sequence Diagram

sequenceDiagram
    actor User
    participant CLI as CLI Command
    participant Resolver as resolveDatabaseUrl
    participant Flag as Flag Parser
    participant Env as Environment
    participant Supabase as Supabase Binary
    participant Prompt as Interactive Prompt
    participant Config as stash.config.ts
    participant DB as Database Client

    User->>CLI: stash db push --database-url postgresql://...
    CLI->>Resolver: resolveDatabaseUrl({databaseUrlFlag})
    Resolver->>Flag: Validate flag value
    Flag-->>Resolver: Valid URL or error
    alt Flag provided and valid
        Resolver->>Env: Set process.env.DATABASE_URL
        Resolver-->>CLI: Return {url, source: 'flag'}
    else Flag invalid
        Resolver-->>CLI: Log error, exit(1)
    else No flag
        Resolver->>Env: Check process.env.DATABASE_URL
        alt Env exists and non-empty
            Resolver-->>CLI: Return {url, source: 'env'}
        else Env missing/empty
            Resolver->>Supabase: Run supabase status --output env
            alt Supabase success and DB_URL found
                Resolver->>Env: Set process.env.DATABASE_URL
                Resolver-->>CLI: Return {url, source: 'supabase-status'}
            else Supabase fails or no DB_URL
                Resolver->>Prompt: Check CI and TTY
                alt Not CI and TTY available
                    Resolver->>Prompt: Show interactive prompt
                    User->>Prompt: Paste URL or cancel
                    alt User provides URL
                        Prompt-->>Resolver: URL
                        Resolver->>Env: Set process.env.DATABASE_URL
                        Resolver-->>CLI: Return {url, source: 'prompt'}
                    else User cancels
                        Resolver-->>CLI: Exit(0)
                    end
                else CI=true or non-TTY
                    Resolver-->>CLI: Log error, exit(1)
                end
            end
        end
    end
    CLI->>Config: Load stash.config.ts
    Config->>DB: Connect using process.env.DATABASE_URL
    DB-->>Config: Connection established
    Config-->>CLI: Config loaded
    CLI->>User: Execute command
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • auxesis

Poem

🐰 A URL found, no need to shout,
Flag, env, Supabase—we work it out!
When all else fails, I'll ask you please,
Paste that connection, set yourself at ease!
No more cryptic config despair,
Your database awaits with care!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(cli): layered DATABASE_URL resolution for db / schema commands' clearly and accurately summarizes the main change—adding a layered resolver for DATABASE_URL across affected CLI commands.
Linked Issues check ✅ Passed The pull request fully implements all coding requirements from issue #376: layered resolver (flag→env→supabase→prompt), source logging, dotenv hints, CI/TTY guards, and all seven affected commands (db install/push/upgrade/status/validate/test-connection, schema build).
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #376's objectives. The PR intentionally excludes init/build-schema (noted as out-of-scope), reformatting-only changes are incidental, and no unrelated features are introduced.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dan/database-url-resolution

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
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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/test-connection.ts (1)

48-54: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update the failure hint to include the new URL sources.

After this change, connection failures can come from --database-url, the env var, or Supabase discovery. Pointing users only at stash.config.ts / .env is now misleading.

💡 Suggested fix
-    p.log.info('Check your databaseUrl in stash.config.ts or .env file.')
+    p.log.info(
+      'Check the resolved DATABASE_URL source (--database-url, .env, or Supabase discovery).',
+    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/db/test-connection.ts` around lines 48 - 54, Update
the failure hint printed in test-connection.ts so it reflects all possible URL
sources instead of only stash.config.ts/.env: modify the p.log.info call in the
error branch (the block that constructs message and calls p.log.error) to
mention --database-url, the environment variable, and Supabase discovery as
places to check; locate the error-handling block that defines message and calls
p.log.error / p.log.info and change the info text accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli/src/config/database-url.ts`:
- Around line 160-161: The CI detection currently only checks process.env.CI ===
'true', causing CI values like '1' or other truthy strings to be missed; update
the isCi computation (used alongside isInteractive) to treat common truthy CI
values case-insensitively (e.g., '1', 'true', 'yes') or simply check for a
non-empty process.env.CI value (e.g., Boolean(process.env.CI) or a regex match)
so that isInteractive (which uses process.stdin.isTTY && !isCi) behaves
correctly in CI environments.

---

Outside diff comments:
In `@packages/cli/src/commands/db/test-connection.ts`:
- Around line 48-54: Update the failure hint printed in test-connection.ts so it
reflects all possible URL sources instead of only stash.config.ts/.env: modify
the p.log.info call in the error branch (the block that constructs message and
calls p.log.error) to mention --database-url, the environment variable, and
Supabase discovery as places to check; locate the error-handling block that
defines message and calls p.log.error / p.log.info and change the info text
accordingly.
🪄 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: 7896bf05-8179-4618-9876-ae1f3978a98b

📥 Commits

Reviewing files that changed from the base of the PR and between 3d12510 and d7bd47c.

📒 Files selected for processing (12)
  • packages/cli/src/__tests__/database-url.test.ts
  • packages/cli/src/bin/stash.ts
  • packages/cli/src/commands/db/install.ts
  • packages/cli/src/commands/db/push.ts
  • packages/cli/src/commands/db/status.ts
  • packages/cli/src/commands/db/test-connection.ts
  • packages/cli/src/commands/db/upgrade.ts
  • packages/cli/src/commands/db/validate.ts
  • packages/cli/src/commands/schema/build.ts
  • packages/cli/src/config/database-url.ts
  • packages/cli/src/messages.ts
  • packages/cli/tests/e2e/database-url.e2e.test.ts

Comment on lines +160 to +161
const isCi = process.env.CI === 'true'
const isInteractive = Boolean(process.stdin.isTTY) && !isCi
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize CI detection beyond exact 'true'.

At Line 160, CI is treated as enabled only when process.env.CI === 'true'. Environments using values like CI=1 can incorrectly fall into interactive behavior on TTY runs.

Suggested patch
-  const isCi = process.env.CI === 'true'
+  const ci = process.env.CI
+  const isCi = typeof ci === 'string' && /^(1|true)$/i.test(ci.trim())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const isCi = process.env.CI === 'true'
const isInteractive = Boolean(process.stdin.isTTY) && !isCi
const ci = process.env.CI
const isCi = typeof ci === 'string' && /^(1|true)$/i.test(ci.trim())
const isInteractive = Boolean(process.stdin.isTTY) && !isCi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/config/database-url.ts` around lines 160 - 161, The CI
detection currently only checks process.env.CI === 'true', causing CI values
like '1' or other truthy strings to be missed; update the isCi computation (used
alongside isInteractive) to treat common truthy CI values case-insensitively
(e.g., '1', 'true', 'yes') or simply check for a non-empty process.env.CI value
(e.g., Boolean(process.env.CI) or a regex match) so that isInteractive (which
uses process.stdin.isTTY && !isCi) behaves correctly in CI environments.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a layered DATABASE_URL resolver to improve onboarding for DB-touching CLI commands by supporting multiple sources (flag/env/Supabase/prompt) before stash.config.ts is loaded, plus tests to cover the new resolution paths.

Changes:

  • Introduce resolveDatabaseUrl() and wire it into DB/schema commands, including a new --database-url flag.
  • Add user-facing message handles for resolver output and failure modes.
  • Add unit + E2E coverage for resolver behavior (flag + CI guard).

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/cli/src/config/database-url.ts New layered resolver for DATABASE_URL, including Supabase and prompt fallbacks.
packages/cli/src/bin/stash.ts Plumbs --database-url into db/schema commands and updates help text.
packages/cli/src/messages.ts Adds message handles for resolver logging/prompts/errors.
packages/cli/src/commands/db/install.ts Runs resolver before config load; accepts databaseUrl option.
packages/cli/src/commands/db/upgrade.ts Runs resolver before config load; accepts databaseUrl option.
packages/cli/src/commands/db/validate.ts Runs resolver before config load; accepts databaseUrl option.
packages/cli/src/commands/db/status.ts Runs resolver before config load; accepts databaseUrl option.
packages/cli/src/commands/db/test-connection.ts Runs resolver before config load; accepts databaseUrl option.
packages/cli/src/commands/db/push.ts Adds databaseUrl option and calls resolver before config load.
packages/cli/src/commands/schema/build.ts Runs resolver before config load; accepts databaseUrl option.
packages/cli/src/tests/database-url.test.ts New unit tests for resolver source ordering and guards.
packages/cli/tests/e2e/database-url.e2e.test.ts New E2E tests validating flag path + CI fail-fast behavior.

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

Comment on lines +39 to +40
await resolveDatabaseUrl({ databaseUrlFlag: options.databaseUrl })

Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

resolveDatabaseUrl is called here but not imported in this module, which will cause a build/TypeScript error at runtime/compile time. Add the missing import (consistent with the other db commands) or remove the call if intentional.

Suggested change
await resolveDatabaseUrl({ databaseUrlFlag: options.databaseUrl })

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

stash db commands fail with cryptic error when DATABASE_URL is not exported

2 participants