feat(cli): add config pull command (CLI-2064) - #6438
Draft
Coly010 wants to merge 7 commits into
Draft
Conversation
…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.
Contributor
Author
|
/ai-review |
Contributor
There was a problem hiding this comment.
🤖 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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 localconfig.toml/config.json, reusing theconfig diffcomparison core (CLI-2156) for classification and--dry-run.How it writes
[remotes.*]block whose rawproject_idliteral 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-labeloverrides; label collisions error rather than silently retargeting an existing block); else the config root, with warnings ondualScopeproperties (new registry metadata) that also configuresupabase start.@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.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;--yesanswers the prompt but never bypasses that guard.Notable decisions (details in ADR 0023 and the Linear issue)
--project-refflag accepting ref/branch/UUID per the settled CLI-2167 vocabulary (the issue text's separate--targetwas dropped).[remotes.*].project_idspelled asenv(...)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-labelis the escape hatch; CLI-2291 tracks a load-time warning.config pushcannot send them back.go-cli-porting-status.mdrow: the file now records only the residual Go delegation surface, and this command is net-new TS. User docs live atapps/cli/docs/supabase/config/pull.md.Found while dogfooding against staging
Pulling
auth.sms.twilio.enabled = truewhile the required siblingaccount_sidstayed 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 reasonwould_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
[remotes.*]matching rule is now exported once from@supabase/config(remoteNameForProjectRef, raw-literal semantics) andconfig diff's reload precheck uses it — anenv()-valuedproject_idno longer triggers a wasted reload with duplicated deprecation warnings. Diff's behavior is otherwise byte-identical (its full suite passes unchanged).config.target.ts,config.format.ts) with per-command error constructors, per the Hoist-Before-You-Duplicate rule.dualScoperegistry metadata (36 paths, snapshot-pinned),writeCliConfigDocumentTextatomic writer, and a git dirty-check helper.Reviewer notes
legacyRunConfigPullwith an injected target and a constructor-produced source) for the plannedsupabase pullorchestrator.supabase/supabase'scommon-cli-sections.jsonneeds aconfig pullentry when the CLI docs next sync.--onlysubset pulls), CLI-2291 (load-time env()-project_id warning), CLI-2292 (config family cleanup). Pre-existing CLI-2285 (--workdirclimb) applies to pull the same way it does to diff/push and stays a separate fix.Fixes CLI-2064