Skip to content

feat(cli): add config diff command - #6295

Draft
kanadgupta wants to merge 4 commits into
kanad-claude/config-default-values-mapping-ced354from
kanadgupta/cli-2156-add-supabase-config-diff-to-the-cli
Draft

feat(cli): add config diff command#6295
kanadgupta wants to merge 4 commits into
kanad-claude/config-default-values-mapping-ced354from
kanadgupta/cli-2156-add-supabase-config-diff-to-the-cli

Conversation

@kanadgupta

Copy link
Copy Markdown
Member

Implements CLI-2156: a read-only supabase config diff that classifies drift between supabase/config.toml and the effective configuration GET /v2/projects/{ref}/config reports for a target project or branch. Never writes. Stacked on the CLI-2155 branch (sparse subtraction / versioned defaults), which it depends on.

What changed

packages/config — the reusable comparison core (ADR 0019)

  • config-diff.ts: pure classifier producing a typed ConfigChangeSet (update / remote_only / local_only, plus masked and scope), with order-insensitive multiset array equality and type-aware scalar comparison ("8080" vs 8080, "true" vs true).
  • config-diff.managed.ts + config-diff.auth.ts: the managed surface as a translation table — any schema path the table can read from the v2 response is managed; everything else ([studio], ports, image pins, [realtime] locals, …) is unmanaged by construction, so the managed set and the response-reading code cannot drift apart. The auth table (188 entries) is ported from the Go CLI's FromRemoteAuthConfig at 7b469f5b3, including inversions (enable_signup!disable_signup), duration/enum transforms, SMS provider fan-out, and Apple/Google client-id joins.
  • Secrets (32 paths, the union of schema x-secret and Go's Secret machinery) are "present, unknown": never compared, never counted, surfaced via masked so a clean diff is visibly a partial claim.
  • remote_only values equal to the schema default are suppressed (the CLI-2155 defaults baseline); optional-key paths with no materialized default suppress the type's zero value instead — otherwise every unconfigured OAuth provider reads as drift.
  • io.ts/lib/env.ts: value origins now record the resolving env-var name, so a change on an env()-fed property names the variable.

apps/cli — legacy-shell command

  • legacy/commands/config/diff/: command + handler + errors + SIDE_EFFECTS.md. Target resolution: --target <branch-name|uuid|ref> (same acceptance as link; 404 → "run supabase branches list"), --project-ref, else the linked ref; --target + --project-ref together is a hard error. When the resolved ref matches a [remotes.*] block's project_id, the local operand is the branch's merged effective config (ADR 0018), otherwise the base config — the echoed scope line always says which.
  • Output: text (unset renders (unset) / (not returned); (from env VAR) annotations; masked note) and --output-format json|stream-json (structured payload with schema_version, target, scope, changes[], masked[], counts). The Go-compat -o/--output flag is rejected outright (every value, pretty included) with an error pointing at --output-format — per the ticket-thread decision with Colum that net-new commands carry no Go parity contract. --exit-code sets exit 1 on drift via ProcessControl.setExitCode after the payload is emitted.
  • legacy/shared/legacy-branch-ref.resolver.ts: the branch name/UUID/ref resolver hoisted out of the branches family (cross-family use) with injected error mappers; the branches family keeps a thin binding so its call sites are unchanged.

Docs: ADR 0019 (classification + managed surface), go-cli-divergences.md TS-only command entry (replacing the ticket's stale go-cli-porting-status.md criterion — that file no longer has a per-command table), per-command SIDE_EFFECTS.md.

Decisions & assumptions worth reviewing

  1. Managed surface = translation table (vs schema annotations or response-key walking): single source of truth; a forgotten entry fails safe (silently unmanaged, never misreported). Reviewed alternatives in ADR 0019.
  2. Zero-value fallback for optional-key paths with no materialized schema default (suppress false/""/0/[] remotes). Judgment call made during implementation — without it the diff floods with unconfigured-provider noise; the cost is that a genuinely zero-valued remote override on such a path is not reported.
  3. schema_version in the payload is the file's $schema ref (falling back to the current schema URL) — CLI-2155 shipped no separate version token.
  4. Legacy -o support was implemented per the original acceptance criteria, then removed after Colum confirmed on the ticket that parity isn't a goal for net-new commands. The flag now fails fast with a bespoke invalid-input error; the JSON payload always carries explicit null for unset sides.
  5. Masked credentials are transparent: listed in masked[] / a text note rather than silently skipped, and never affect --exit-code.
  6. Partial responses degrade, never error: managed paths the response doesn't carry are local_only when declared locally, silent otherwise; the scope line calls out missing blocks. (Today's v2 schema requires all six blocks, so this is belt-and-braces for API evolution and permission-trimmed keys.)
  7. Overlap with config push's config-sync/ noted, not consolidated: those helpers render Go-parity per-service diff text against v1 endpoints for push previews. Consolidating push onto this core is a possible follow-up.
  8. Deliberately unmapped wire keys (no local schema path): passkey/webauthn, figma provider, sms_test_otp, mailer template contents, OAuth-server keys, third-party auth.

🤖 Generated with Claude Code

kanadgupta and others added 4 commits August 20, 2026 15:17
Adds the pure comparison engine for supabase config diff: a managed-surface
table (defined by the v2 project-config translation, so unmapped schema paths
are unmanaged by construction), a change-set classifier with update /
remote_only / local_only classes, order-insensitive type-aware equality,
byte-size canonicalization, masked-secret transparency, and env-var name
threading through the interpolation pipeline onto value origins.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… ADR 0019 (CLI-2156)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Read-only drift report between supabase/config.toml and the effective
configuration GET /v2/projects/{ref}/config reports for a target project or
branch. Target resolution via --target (branch name/UUID/ref, link-style
acceptance) or --project-ref or the linked ref; matching [remotes.*] blocks
become the merged local operand per ADR 0018. Text, --output-format
json/stream-json, and Go-compat -o encodings share one structured payload;
--exit-code flips exit 1 on drift after the payload is out. Hoists the branch
name/UUID resolver to legacy/shared with injected error mappers. Adds ADR
0019, SIDE_EFFECTS.md, a go-cli-divergences entry, 26 integration tests
(handler at 100% branch coverage), format unit tests, and a live golden path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Colum confirmed on the ticket that net-new commands carry no Go parity
contract, so the Go-compat -o/--output flag is now rejected outright (every
value, pretty included) with an error pointing at --output-format, failing
fast before target resolution or any network call. Drops the four Go-encoder
emit branches, simplifies the JSON payload to always carry explicit nulls for
unset sides, and updates SIDE_EFFECTS.md, the divergences entry, and the
tests. Ticket acceptance criteria amended accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant