Repository navigation
feat(cli)!: one --remote flag, local target defaults and one hosted-mutation consent rule - #7188
Conversation
…he local target first
…s after the retries run out
…r can match A nested comment closed by */ directly followed by * re-used the slash as the start of /*, so the splitter stayed inside a comment PostgreSQL had already closed. /*/ also no longer closes the comment it opens. Adds an unterminated-input flag for callers that must refuse such input.
…ck waits --read-only now accepts only statements that start with SELECT, WITH, VALUES, TABLE, SHOW or EXPLAIN, refuses input that ends inside a comment, quote or dollar quote or contains BEGIN ATOMIC, and sets a 5s lock_timeout inside the read-only transaction on direct connections. Docs and SIDE_EFFECTS state what the flag does and does not guarantee.
…-project-id removal
…tions - Hosted changes accept only `--yes` or a shell-exported SUPABASE_YES; a value that a cwd `.env`, `.env.local` or `.env.$NODE_ENV[.local]` also defines counts as file-sourced. - The confirmation echo names the real source (`--yes` or `SUPABASE_YES`). - The consent-required error names the action, target and reason, with suggestions for `--yes`, `--read-only` (db query), ignored dotenv files and agent runs. - Action strings are imperative sentence-case phrases at every call site. - `canPrompt` refuses when an agent is detected; `notebooks pull` reconcile uses it. - Global `--yes` help describes the hosted rule. - A sync or selectable leaf with `consent: "none"` must declare `consentExemptReason` (type and tree guard); notebooks pull's create operation is exempt for that leaf only. - ADR 0033 matches the code: groups, approval sources, agent rule, exemptions.
# Conflicts: # apps/cli/src/commands/gen/types/SIDE_EFFECTS.md
…default-local hint Show "branch" only on positive evidence, name a ref that is not the linked project, append a --linked hint to the default-local banner, add the port to the db-url banner, encode every JSON target through a CommandTargetJson schema, share withAnnouncedTarget, and build the linked ResolvedTarget in resolveTarget. config push reads its JSON target environment from the live probe and prints its plan lines in plan wording.
Every loader reports a malformed config file through describeConfigParseError, resolveTarget hints at --linked only for a default-local run on a linked checkout, resolveConfigTarget has integration coverage for its linked target, and the command target banner and JSON shape live in one shared doc that the SIDE_EFFECTS files link.
# Conflicts: # apps/cli/src/commands/config/push/push.integration.test.ts # apps/cli/src/commands/secrets/set/set.integration.test.ts
There was a problem hiding this comment.
Superseded by a newer AI review
🤖 AI Review
Verified all four findings against the PR code. Confirmed the inaccurate reset plan, failing read-only live-test assertion, and misleading linked-target hint. Refuted the reported consent-bypass example: parseDotEnv expands the same-file reference to "1", so the consent check recognizes it as dotenv-derived. Focused Bun checks verified the parser behavior and SQL rejection; the live suite was not run.
Findings
| Severity | Location | Category | Sources | Claim |
|---|---|---|---|---|
| 🟡 MINOR | apps/cli/src/commands/db/reset/reset.handler.ts:243 |
ux |
claude | The remote reset confirmation promises to apply local migrations even when migrations are disabled in config. |
| 🟡 MINOR | apps/cli/src/commands/db/query/query.live.test.ts:48 |
test-correctness |
codex | The new read-only live test expects a database error, but its CREATE statement is rejected by the client-side filter before execution. |
| 🟡 MINOR | apps/cli/src/command-internal/target/resolve-target.ts:75 |
target-selection |
codex | The default-local hint can name a different project from the project that --linked selects when SUPABASE_PROJECT_ID overrides the saved link. |
Refuted findings (kept for transparency, not posted as review comments)
apps/cli/src/command-internal/hosted-consent/hosted-consent.yes.ts:74(security): Dotenv variable expansion can silently approve hosted mutations because APPROVAL=1 followed by SUPABASE_YES=$APPROVAL allegedly parses as a literal reference while Bun supplies "1".
Refuted: Running the actual parseDotEnv implementation on the reported input returns SUPABASE_YES="1", not "$APPROVAL". That equals the reported Bun process value, so hosted-consent.yes.ts:74-77 returns no approval. The alleged literal-versus-expanded mismatch does not occur for this example.
Stats
Claude findings: 1 · Codex findings: 3 · Confirmed: 3 · Refuted: 1 · Uncertain: 0
Models: claude-opus-5-5 + gpt-6.1-sol · Trigger: auto · Workflow run
This review runs once per PR. A maintainer can request another with a /ai-review comment.
Group help tags every subcommand row as local or remote, local only or remote only, and a command's own help gets a TARGET section before USAGE. The --remote flag row shows the value it accepts. JSON help carries the same information as a target field.
… and drop overlapping cases
…time layer comments
…y dotenv file defines it
…bound --remote cases
…t is not a branch
…bc-consistent-local-vs-remote-target-defaults-and-one
…bc-consistent-local-vs-remote-target-defaults-and-one
…whatever its value
|
Thanks for dogfooding it on staging @avallete, that's really reassuring! Went through the notes: Worth a look: all fixed
Minor:
Cleanup: done, apart from one bit
|
Summary
Commands that can run against either the local stack or a hosted project didn't agree on what they target by default, and each took a different mix of
--local,--linked,--project-refand--db-url. Hosted mutations also each had their own idea of when to ask. This makes both consistent: every command that can run locally runs locally unless you pass--remote, and every hosted mutation follows one consent rule.Design: CLI-2695: one
--remoteflag. Decisions and enforcement: ADR 0033.Targets
One flag,
--remote, on every command that can reach a hosted project:--remote)SUPABASE_PROJECT_ID, then the link file)true,yes,on,1,y--remotefalse,no,off,0,n--remoteout insteadpostgres://…/postgresql://…supabase link's rule (a branch UUID also works without a link)--remote=<value>always works.--remote <value>is rewritten to the=form before parsing wherever it's unambiguous. When a bare--remoteis followed by a word the command takes as an operand and that word is, or nearly is, a branch name, or is shaped like a project ref, the command stops before doing anything and shows both readings.--remotegiven twice with different values fails before anything runs.Database commands (db, migration, inspect, storage, seed buckets, test db, gen types, db schema declarative generate) default to the local stack. In a linked checkout, a bare run says
default; pass --remote for <project>.Hosted-only commands (functions, secrets, config, branches, sso, …) keep the linked project as their default and take
--remote=<ref|branch>.branches *takes--remote=<ref>for the parent project.--linked,--project-ref,--db-urlandgen types --project-idstill work, routed through--remote, and print a deprecation warning on stderr naming the--remotereplacement and v4 as the removal release.--localis accepted, does nothing, and warns. They're hidden from help.db diff --from/--totake the same values pluslocalandmigrations;remoteis the linked project andlinkedis deprecated. The deprecated flags still work alongside them and warn:--project-refpoints theremoteside at that project, and the others change nothing.A hosted database whose direct host is unreachable falls back to its connection pooler: first the pooler URL
supabase linksaved, then the project's pooler config from the Management API. The linked project used to try only the saved URL.--remote=<default branch>is reported as production, not as a branch. A branch UUID used without a link is reported by its project ref alone, since the CLI can't tell whether it's the default branch.Every command declares its target group with
withTargetPolicy(local-only, account, hosted, selectable); a tree test fails if a leaf doesn't. Help shows aTARGETsection and labels subcommands local only / remote only / local or remote.Commands that resolve a target print one
Target: …line on stderr (db diff --from/--toprints one per database, in order), and JSON results carry atargetobject.Hosted consent
--yesor a shell-exportedSUPABASE_YES. ASUPABASE_YESfromsupabase/.envor a working-directory.envdoesn't count, stdin is never read as an answer, and agents never get a prompt.--yes, and a failed removal is reported as that failure.pullasks once, up front, on every run except--dry-run.db pullasks when it would update the remote migration history. Declining either aborts before any file is written.New
db query --read-onlyaccepts only read statements (SELECT,WITH,VALUES,TABLE,SHOW,EXPLAINor a parenthesised query), runs them in a read-only transaction with a short lock timeout (or the Management API'sread_onlymode on a hosted target), and skips consent. Functions with side effects called from aSELECTstill run, and the docs say so.The companion PR to
develop, #7186, warns on bare runs of every command whose default flips here.Linked issue
Closes CLI-2695
🤖 Generated with Claude Code
BREAKING CHANGE: Commands that can run locally now default to the local stack,
--remotereplaces the target flags, and every hosted mutation follows one consent rule.db push,db pull,migration fetch,db dump,migration list,migration repair,inspect db *,inspect report,gen typesandstorage ls/cp/mv/rmnow target the local stack when run without a target flag. Add--remoteto reach the linked project (supabase db push --remote),--remote=<ref>for another project,--remote=<branch>for a branch of the linked project, or--remote=<postgres-url>for a database URL.SUPABASE_PROJECT_IDon its own no longer selects a hosted project for these commands; it only says which project bare--remotemeans.gen typeswith no target generates from the local database instead of opening a project picker or failing. In a linked checkout with no local stack running, it suggests--remote.db schema declarative generateno longer asks which target to use.db schema declarative syncwith no declarative files generates them from the local database and no longer offers the linked project as the source; runsupabase db schema declarative generate --remotefirst to start from the linked project.--linked,--project-ref,--db-urlandgen types --project-idare deprecated: they still work and print a warning on stderr naming the--remoteform.--localdoes nothing and warns. v4 removes all of them, along withdb diff'slinked(useremote). They no longer appear in help.--remote=<value>can't be combined with any of the deprecated flags.--project-refon a database command no longer needs--linked. An empty--project-ref=is now an error instead of being ignored.--passwordonly applies to a hosted target.db diffrejects--remotetogether with--from/--to; use--from=remoteor--to=remote.db queryrejects SQL passed both as an argument and with--file.--remote(and the deprecated--linked) opens the project picker in an interactive terminal when the checkout isn't linked. Database and storage commands used to fail.--remote <word>after a command that takes an operand fails with a "did you mean" error when<word>is, or is close to, a branch name of the linked project, when it's shaped like a project ref, or when the branch list can't be fetched. Put the operand before--remote(supabase db pull add_profiles --remote) or use--remote=<branch>.HostedMutationConsentRequiredErrorin non-interactive, JSON, stream-json, agent or-o json|yaml|toml|envruns unless--yes(orSUPABASE_YESexported in the shell) is set. Commands that newly need this for unattended runs:--dry-run--read-onlyfor reads)ypiped on stdin no longer confirms a hosted mutation.SUPABASE_YESinsupabase/.env, or in a.env,.env.localor.env.<NODE_ENV>[.local]file in the working directory, no longer confirms hosted mutations. When a working-directory file defines it at all, a shell-exportedSUPABASE_YESdoesn't count either, since the CLI can't tell the two apart. Use--yes.--prunenow runs even when every function is already up to date.secrets unset --alllists the secrets before asking.HostedMutationDeclinedError("context canceled"). The command-specific cancelled error codes are removed. Decliningdb pullorpullaborts before any file is written; there's no longer a files-only path.--yes. Otherwise the command fails and points tosupabase network-bans remove.migration downandmigration repair --allon the local database no longer prompt.Target: …once on stderr, including read-only commands against a hosted project.config pushno longer prints its own "Pushing config to" line. JSON object results and error envelopes gain atargetobject;db reset'stargetchanges from a string to that object, andconfig pushJSON no longer reportsdeclined_addonsor a skipped status. Arrays and generated artifacts are unchanged.migration upagainst a hosted target reports "Remote database is up to date." instead of "Local database is up to date.", andmigration listlabels the history column "Local DB" when the target is local.--yesfor hosted mutations.