Skip to content

feat(cli): add config pull command (CLI-2064) - #6438

Draft
Coly010 wants to merge 7 commits into
developfrom
columferry/cli-2064-add-supabase-config-pull-to-the-cli
Draft

feat(cli): add config pull command (CLI-2064)#6438
Coly010 wants to merge 7 commits into
developfrom
columferry/cli-2064-add-supabase-config-pull-to-the-cli

Conversation

@Coly010

@Coly010 Coly010 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

What

Adds supabase config pull (CLI-2064): fetches the config the Management API reports for a target project or branch (GET /v2/projects/{ref}/config) and applies it to the local config.toml/config.json, reusing the config diff comparison core (CLI-2156) for classification and --dry-run.

supabase config pull                       # linked project → config root
supabase config pull --project-ref staging # branch → [remotes.staging]
supabase config pull --dry-run             # preview, equivalent to config diff
supabase config pull --yes                 # skip confirmation (CI)

How it writes

  • Scope resolution (ADR 0023): reuse the [remotes.*] block whose raw project_id literal matches the resolved ref (the same rule the loader's overlay uses, so pull and push stay inverses); else a branch-named target creates [remotes.<branch-name>] (--remote-label overrides; label collisions error rather than silently retargeting an existing block); else the config root, with warnings on dualScope properties (new registry metadata) that also configure supabase start.
  • Surgical writes: a new format-preserving editor in @supabase/config (config-edit.ts) splices values into the raw TOML text — comments, blank lines, ordering, and quote style survive. Every edit is verified by re-parsing and deep-comparing against the independently computed expected document before an atomic temp+rename write; anything the editor can't handle safely is a typed refusal, never a best-effort splice. JSON configs are re-serialized with detected indent and preserved key order.
  • Never harms the file: pull never removes properties; local_only/unmanaged paths are untouched; env() references are never replaced with literals (reported with the variable name); masked credentials are never written; a plan fixpoint + pre-write schema-validation gate guarantee the written file always re-loads (see below). Uncommitted/untracked changes to the config abort non-interactive runs unless --force; --yes answers the prompt but never bypasses that guard.
  • Convergence: running pull twice against an unchanged remote writes nothing the second time and leaves the file byte-identical — verified live against staging.

Notable decisions (details in ADR 0023 and the Linear issue)

  • Single --project-ref flag accepting ref/branch/UUID per the settled CLI-2167 vocabulary (the issue text's separate --target was dropped).
  • An [remotes.*].project_id spelled as env(...) that resolves to the target is a hard error: the loader matches raw literals, so writing there would never take effect and pull would never converge. --remote-label is the escape hatch; CLI-2291 tracks a load-time warning.
  • ADR-0021 "unpushable" families are written, with a note that config push cannot send them back.
  • No go-cli-porting-status.md row: the file now records only the residual Go delegation surface, and this command is net-new TS. User docs live at apps/cli/docs/supabase/config/pull.md.

Found while dogfooding against staging

Pulling auth.sms.twilio.enabled = true while the required sibling account_sid stayed gated (declared-but-unpushable against the pre-write state) produced a config that failed every subsequent schema load. Fixed with two layers: the plan expands to a fixpoint (re-classifying after projecting writes, so un-gated siblings get pulled too), and a pre-write validation gate decodes the projected document with the real schema and drops any family that would not re-load (skip reason would_invalidate, naming the missing fields) instead of writing it. Both layers verified live: the same scenario now withholds the twilio family with an actionable note while the other changes apply, and the file always re-loads.

Also in this PR

  • Fixes CLI-2287: the [remotes.*] matching rule is now exported once from @supabase/config (remoteNameForProjectRef, raw-literal semantics) and config diff's reload precheck uses it — an env()-valued project_id no longer triggers a wasted reload with duplicated deprecation warnings. Diff's behavior is otherwise byte-identical (its full suite passes unchanged).
  • Hoists diff's target resolution and shared formatters to the config family root (config.target.ts, config.format.ts) with per-command error constructors, per the Hoist-Before-You-Duplicate rule.
  • New dualScope registry metadata (36 paths, snapshot-pinned), writeCliConfigDocumentText atomic writer, and a git dirty-check helper.

Reviewer notes

  • The handler is callable as a library (legacyRunConfigPull with an injected target and a constructor-produced source) for the planned supabase pull orchestrator.
  • Docs-site follow-up (not in this repo): supabase/supabase's common-cli-sections.json needs a config pull entry when the CLI docs next sync.
  • Follow-ups filed: CLI-2289 (push branch-name vocabulary so the pull/push round trip has no manual step), CLI-2290 (--only subset pulls), CLI-2291 (load-time env()-project_id warning), CLI-2292 (config family cleanup). Pre-existing CLI-2285 (--workdir climb) applies to pull the same way it does to diff/push and stays a separate fix.

Fixes CLI-2064

…ing, dualScope registry (CLI-2064)

Wave 1 of supabase config pull: format-preserving config-edit engine with
re-parse verification, rawDocument/interpolatedRemotes on LoadedCliConfig with
the exported remoteNameForProjectRef raw-literal rule (fixes the CLI-2287
precheck drift), atomic writeCliConfigDocumentText with typed CliConfigWriteError,
dualScope registry metadata, git dirty-check helper, and the config
target/format hoist out of the diff command.
Scope resolution (raw-literal block reuse, label collisions, env() project_id
refusal), write planner (env_reference/local_only/unwritable skips, dual-scope
and drift warnings), text/JSON formatters with payload v1, 14 classified error
types, and the ADR recording the write strategy and scope rule.
Handler implements the 15-step flow: -o rejection, base load + single file-text
snapshot, shared target resolution, destination resolution with stderr
announcement, conditional overlay reload, v2 config fetch + diff, write
planning with in-memory convergence check (unpushable-family note, planner
defect guard), dry-run, git dirty guard, confirmation, TOCTOU re-read,
surgical edit + atomic write, and instrumentation/cache/flush invariants.
Exposes legacyRunConfigPull as the library seam for the future pull
orchestrator.
… fixes (CLI-2064)

71-case integration matrix covering every acceptance criterion, SIDE_EFFECTS.md,
user docs page, AGENTS.md precedent updates, and two step-order fixes the
matrix surfaced: the git dirty guard now only runs when a write will happen
(converged runs spawn no git subprocess and exit 0), and a zero-drift branch
target still creates its [remotes.*] tracking block so block reuse engages on
subsequent runs.
Review batch from engineer/architect/DX passes: branch-derived label
collisions no longer silently retarget an existing [remotes.*] block
(sanitized-label collision rule shared with --remote-label); --remote-label
is honored above the env() project_id refusal so the error's escape hatch is
real; --yes no longer bypasses the uncommitted-changes guard on a TTY;
convergence residuals are typed errors instead of defects; the surgical
editor preserves trailing inline comments, mixed EOLs, JSON newline flavor,
and sibling indentation, with a real byte-preservation property test;
writeCliConfigDocumentText demoted to the internal entrypoint; message,
docs, and ADR 0023 corrections throughout.
…2064)

Dogfooding against staging surfaced a bricking bug: pulling a provider
toggle (auth.sms.twilio.enabled = true) without its ADR-0021-gated siblings
left config.toml failing schema load on every subsequent command. Two
layers: the plan now expands to a fixpoint (re-classifying after projecting
writes so un-gated siblings like account_sid are pulled too), and a
pre-write validation gate decodes the projected document with the real
schema, dropping any family that would not re-load (skip reason
would_invalidate, naming the missing fields and env vars) instead of
writing it. Also registers --remote-label in VALUE_CONSUMING_LONG_FLAGS.
@Coly010

Coly010 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

/ai-review

@github-actions github-actions Bot 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.

🤖 AI Review

Verified all 13 reported findings and merged two overlapping pairs into 11 entries. Ten findings are confirmed: four major correctness/data-loss risks, three minor reporting/documentation issues, and three convention/documentation nits. The multi-line-array comment finding is refuted because the editor explicitly documents and tests replacement of the entire array value span.

Findings

Severity Location Category Sources Claim
🟠 MAJOR apps/cli/src/legacy/commands/config/pull/pull.handler.ts:465 validation claude+codex The pre-write validation gate can abort valid pulls when a numeric or boolean env(VAR) value is supplied only by the project's dotenv files.
🟠 MAJOR apps/cli/src/legacy/commands/config/pull/pull.handler.ts:757 validation claude+codex Remote-destination pulls are validated without merging the selected remote overlay, allowing the command to write a block that fails business rules when subsequently loaded for that project.
🟠 MAJOR packages/config/src/config-edit.ts:1023 correctness codex The dotted-sibling search mistakes keys inside descendant table headers for dotted-key declarations, causing valid insertions to produce an empty key and be refused.
🟠 MAJOR apps/cli/src/legacy/commands/config/pull/pull.handler.ts:565 concurrency codex The concurrent-edit guard can overwrite changes made between the parsed config load and the separate baseline text read.
🟡 MINOR apps/cli/src/legacy/commands/config/pull/pull.format.ts:332 error-reporting claude A newly created remote block whose differences were all skipped is incorrectly summarized as having no config differences to apply.
🟡 MINOR apps/cli/docs/supabase/config/pull.md:15 documentation claude The documentation incorrectly says a non-TTY text run never prompts and always proceeds as confirmed.
🟡 MINOR apps/cli/src/legacy/commands/config/pull/pull.plan.ts:554 output-correctness codex Path-scoped warnings remain after schema validation drops their associated writes and can falsely report that skipped values were written.
⚪ NIT packages/config/src/config-edit.ts:26 documentation claude The module header points readers to a nonexistent resolveTomlPlacement function.
⚪ NIT packages/config/src/io.ts:897 effect-conventions codex The new Effect code directly inspects a tagged runtime representation through error.reason._tag, contrary to repository conventions.
⚪ NIT apps/cli/src/legacy/commands/config/pull/pull.plan.ts:340 typescript-conventions codex The path comparator uses production as string casts solely to suppress indexed-access typing.
Refuted findings (kept for transparency, not posted as review comments)
  • packages/config/src/config-edit.ts:993 (data-preservation): Replacing a multi-line array unexpectedly violates the editor's comment-preservation contract by deleting comments inside the array.
    Refuted: The deletion itself occurs, but it is neither silent nor contrary to the stated contract: an internal array comment is part of the replaced value span, while preservation is promised for untouched or elsewhere content. The behavior is explicitly documented and pinned by a test.

Stats

Claude findings: 6 · Codex findings: 7 · Confirmed: 10 · Refuted: 1 · Uncertain: 0


Models: claude-opus-5 + gpt-5.6-sol · Trigger: manual · Workflow run

This review runs once per PR. A maintainer can request another with a /ai-review comment.

Comment thread apps/cli/src/legacy/commands/config/pull/pull.handler.ts Outdated
Comment thread apps/cli/src/legacy/commands/config/pull/pull.handler.ts
Comment thread apps/cli/src/legacy/commands/config/pull/pull.format.ts
Comment thread apps/cli/docs/supabase/config/pull.md Outdated
Comment thread packages/config/src/config-edit.ts Outdated
Comment thread packages/config/src/config-edit.ts Outdated
Comment thread apps/cli/src/legacy/commands/config/pull/pull.handler.ts Outdated
Comment thread apps/cli/src/legacy/commands/config/pull/pull.plan.ts Outdated
Comment thread packages/config/src/io.ts Outdated
Comment thread apps/cli/src/legacy/commands/config/pull/pull.plan.ts Outdated
Validation gate now mirrors the loader exactly: env resolved from process env
plus project dotenv (decodeCliConfigDocumentForValidationEffect), remote
destinations validated against both the raw and overlay-merged projections,
and pre-existing decode failures are never attributed to the plan. The write
baseline is the loader's own bytes (LoadedCliConfig.rawText), closing the
load-vs-read race. The editor's dotted-sibling scan no longer refuses valid
inserts under sub-table-only parents. Dropped families take their path-scoped
warnings with them; a created block with all changes skipped is summarized
honestly; non-TTY text prompt behavior documented accurately; _tag inspection
and as-casts replaced with idiomatic narrowing.
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