feat(cli): port supabase stop and status commands to native TypeScript#5765
feat(cli): port supabase stop and status commands to native TypeScript#5765Coly010 wants to merge 90 commits into
Conversation
Replaces the Go-proxy stubs for `stop`/`status` with native Effect implementations that talk directly to Docker/Podman via subprocess, replicating Go's label-filtering and container-naming scheme byte-for-byte. Legacy `start` is still Go-proxied, so this intentionally does not go through `@supabase/stack/effect`'s daemon-based orchestration model — that substrate is incompatible with the real containers Go's binary creates. Adds shared Docker/config infrastructure (`legacy-docker-lifecycle`, `legacy-go-jwt`, `legacy-local-config-values`, `legacy-api-url`) used by both commands, and fixes several correctness issues found during review: stdout colorization keyed off the wrong stream's TTY status, `--override-name` incorrectly leaking into pretty-mode output (Go ignores it there), and the `--backup`/`--no-backup` formula not matching Go's actual (dead-flag) behavior. Fixes CLI-1324
…e.test.ts Documents the *.live.test.ts convention in AGENTS.md (a 4th test category alongside unit/integration/e2e): black-box CLI subprocess tests executed by the cli-e2e-ci harness against a real supabox stack. Clarifies how local-dev-stack commands like stop/status fit this pattern despite never calling the Management API — they only need the real Docker daemon the cli-e2e-ci runner also provides, so they reuse the existing describeLive gate rather than a dedicated one. Adds stop.live.test.ts and status.live.test.ts, each spinning up a real local Docker stack (init -> start, excluding the heaviest services) in an isolated temp directory and verifying the command under test against the real containers, with best-effort cleanup on every path.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Effect boolean flags register both the positive and negated token, so Flag.boolean("backup") above already claims --no-backup as the false form of the hidden backup flag. Defining a separate noBackup boolean on the same token can route supabase stop --no-backup to backup=false or otherwise collide during parsing, leaving flags.noBackup false and skipping the volume-prune path even though the user requested data deletion; model the hidden --backup=false flag without generating a --no-backup alias.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Thanks for flagging this — I dug into it and don't think there's a collision here, though the concern was reasonable to raise given how the two flags look side by side. Effect's flag registry ( I confirmed this empirically by running the exact flag config through
This also matches the Go CLI's own structure: |
…5765) Three accepted findings from PR review, each verified against Go source before implementing: - Sanitize the config/env-derived project id before building the Docker label filter in both `stop` and `status`. Go's `Config.Validate` sanitizes `Config.ProjectId` once at config-load time (pkg/config/config.go:938-944), and every later reader — including the Docker label `start` writes (internal/utils/docker.go:375) — sees that same sanitized string. Without this, a dirty `project_id` (spaces, leading punctuation) would filter on a value `start` never labeled, silently matching nothing. The explicit `--project-id` bypass on `stop` stays raw, matching Go's stop.go:19-20. - `status --override-name` with an unrecognized field key is now silently ignored instead of a hard error, matching Go's `go-env` Unmarshal, which never checks its input map for unmatched keys (verified against the vendored go-env@v0.1.2 source). - `status` no longer hard-fails when `supabase/config.toml` is absent; Go's `flags.LoadConfig` treats a missing file as a no-op and proceeds with template defaults (pkg/config/config.go:655-656), so this now decodes an empty document through the shared config schema for its defaults instead of erroring. (The broader gap — Go's automatic `SUPABASE_<FIELD>` env-var binding via viper, which `@supabase/config` doesn't have an equivalent for — is a larger, cross-cutting `@supabase/config` feature affecting every ported command, called out as a follow-up rather than folded into this fix.) Two other findings were investigated and rejected with cited evidence (directly on the PR): a claimed `--backup`/`--no-backup` flag collision (empirically disproven against Effect's parser), and image-name-based `--exclude` matching (the underlying config.toml schema has no field to check, on either CLI).
…0O86N3Lyo) Go's status.toValues() gates each service on `--exclude` matching either the container id or the Docker image's short name (ShortContainerImageName). The TS port only checked container ids. Port the short-name extraction and check it against the same default images the embedded Dockerfile manifest already provides, since the relevant Go config fields (KongImage, Image, etc.) all carry `toml:"-"` and are never user-overridable.
…us-commands # Conflicts: # apps/cli/docs/go-cli-porting-status.md
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1b42381e79
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1b42381e79
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…OErm0O86N4wLX) Go's stop.Run checks len(projectId) > 0 (internal/stop/stop.go:18), not just whether --project-id was set, so an explicit but empty value falls through to config.toml resolution. The TS port only checked Option.isSome, so `--project-id ""` resolved to the bare all-projects label filter instead.
…6N4wLa, PRRT_kwDOErm0O86N4wLu) Go registers both --override-name and --exclude as pflag StringSliceVar (cmd/status.go:36-37), which CSV-splits each occurrence and accumulates across repeats. The TS flags only handled repetition, so a single comma-separated value like `--exclude kong,auth` produced one malformed entry instead of two. Reuses the shared legacyParseStringSliceFlag already applied to sso/postgres-config for the same StringSlice parity gap.
…6N4wL1) Go's Config.Validate fails config-load when auth.jwt_secret is set but shorter than 16 characters (pkg/config/apikeys.go:45-47), before any command can render output. The TS resolver accepted any non-empty value and signed ANON_KEY/SERVICE_ROLE_KEY with it, letting `status -o env/json` succeed and print keys for a config the Go CLI and local stack both reject.
… PRRT_kwDOErm0O86N4wLx)
Go's config loader binds Viper with SetEnvPrefix("SUPABASE") + AutomaticEnv()
(pkg/config/config.go:529-535), so SUPABASE_AUTH_JWT_SECRET/PUBLISHABLE_KEY/
SECRET_KEY/ANON_KEY/SERVICE_ROLE_KEY override the corresponding config.toml
value at higher precedence. legacyResolveLocalConfigValues only read the
decoded config object, so a local stack started with those env overrides
had `status` print keys that didn't match the running Auth service. Scoped
to exactly the 5 auth fields this module reads, not a general
@supabase/config port of Viper's AutomaticEnv.
…ed (review: PRRT_kwDOErm0O86N4wLk) Go's generateJWT signs anon/service_role with the first key in auth.signing_keys_path (RS256/ES256) instead of HMAC when that file resolves to a non-empty JWK array (pkg/config/apikeys.go:76-113). The TS resolver only ever HMAC-signed with jwt_secret. Ports GenerateAsymmetricJWT via legacyGenerateAsymmetricGoJwt (RFC 7517 JWK -> Node crypto private key, ieee-p1363 signature encoding for ES256's raw r||s format) and wires it into legacyResolveLocalConfigValues, which now needs workdir to resolve a relative signing_keys_path against <workdir>/supabase.
Go's jwkToRSAPrivateKey/jwkToECDSAPrivateKey reject a JWK whose kty doesn't match its claimed alg, and an EC key whose curve isn't P-256 (pkg/config/auth.go). legacyGenerateAsymmetricGoJwt only checked alg, so an EC key forged with alg: RS256 (or a non-P-256 curve claimed as ES256) signed "successfully" and produced a spec-invalid token that silently fails verification instead of raising an error. Found independently by the security and DX reviewers in review-changes.
…tusValues twice status.handler.ts called legacyStatusValues twice per invocation (real values, then pretty-mode values with an empty override map) — only the first call was wrapped in Effect.try. With signing_keys_path support this doubled file I/O + JWT signing on every text-mode run, and left the second call able to throw an uncaught exception if it ever diverged from the first. Splits legacyStatusValues into legacyResolveStatusState (the throwing half: local config resolution + gating) and legacyStatusValuesFromState (pure name remapping), so the handler resolves state once, guards it once, and reuses it for both value maps.
…ration coverage Go's Config.Validate fails a bad auth.signing_keys_path with "failed to read signing keys: %w" (open failure) or "failed to decode signing keys: %w" (parse failure) (pkg/config/config.go:1059-1062). The TS port let readFileSync/JSON.parse's raw Node error text through unwrapped instead. Also adds status.integration.test.ts coverage for the SUPABASE_AUTH_* env override and asymmetric-signing-key behaviors, which previously only had unit-level coverage on the pure resolver.
Supabase CLI previewnpx --yes https://pkg.pr.new/supabase/cli/supabase@3c6d766f35cc6b8d852c94c556a54d243b7b99ebPreview package for commit |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 60877ba497
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…spection (ci: e2e shard 1/3)
Go's assertContainerHealthy never special-cases a missing container — it
wraps whatever ContainerInspect returns, so the real daemon error text
("No such container: ...") flows through. The TS port collapsed that case
into a hardcoded "no such container" string via an "absent" sentinel,
discarding the real text.
Separately, legacyInspectContainerState's Effect.all([exitCode, stdout,
stderr]) ran sequentially by default, awaiting exitCode (Node's "exit"
event) before ever subscribing to the stdout/stderr streams. Node's "exit"
can fire before a fast process's stdio pipes are drained, so the real
Docker CLI's stderr was silently lost in the real subprocess environment
even after removing the hardcoded string — reproduced against a real
docker CLI subprocess, confirmed via runParity's stderr comparison in
apps/cli-e2e. Same fix applied to the two other call sites in this file
that share the pattern (legacyListContainersByLabel, legacyListVolumesByLabel).
…e2e shard 1/3) Go prints this line via an unconditional, immediate fmt.Fprintln before any Docker call runs (docker.go:97), routed straight to stdout in non-interactive mode (tea.go's fakeProgram). The TS port gated it behind the shared output.task spinner's 200ms debounce, so the line was silently dropped whenever the underlying Docker calls resolved faster than that threshold — exactly what happens against the mocked/replayed Docker CLI in the e2e harness. Printing it directly via output.raw removes the race entirely.
…ity request logs (ci: e2e shard 1/3) stop/status parity comparisons were failing on request-log mismatches that reflect nothing about CLI behavior: the real docker CLI issues a fresh HEAD /_ping handshake before every subprocess invocation (Go's SDK pings once per command via a persistent client), and negotiates its own API version segment into the URL path (/v1.51/ vs /v1.53/) based on the installed docker CLI/daemon, not anything the command controls. Strip both before comparing so parity reflects the actual Docker operations performed rather than client plumbing, mirroring the equivalent normalization already used for fixture matching in apps/cli-e2e/src/server/placeholder.ts.
…s (review: PRRT_kwDOErm0O86N7ctR)
Go's Viper binds SetEnvPrefix("SUPABASE") + AutomaticEnv() over every
config field via UnmarshalExact's struct walk (pkg/config/config.go:531-535,
698-705), including the plain-string Auth.SigningKeysPath field
(pkg/config/auth.go:164) — not just the 5 auth fields this module already
wrapped with envOverride. Load() resolves that override before Validate()
opens and parses the JWK file (config.go:735-745, 1059-1062), so an
env-only SUPABASE_AUTH_SIGNING_KEYS_PATH was silently ignored and status
fell back to HMAC-signed keys the running Auth service would reject.
… status (review: PRRT_kwDOErm0O86N7ctY) Go's Config.Load runs loadNestedEnv (supabase/.env and .env.local via godotenv.Load, which never overrides an already-set var) before loadFromFile wires up Viper's AutomaticEnv (pkg/config/config.go:735-745, 528-535) — so an env-file-only SUPABASE_PROJECT_ID overrides config.toml's project_id too, not just an ambient shell export. Both handlers only read process.env directly, missing that middle step: an env-named stack could be left running by stop, or status could target the wrong container. @supabase/config's loadProjectEnvironment already implements the same "ambient wins over .env/.env.local" layering (used today only for env() interpolation inside config.toml), so both handlers now resolve SUPABASE_PROJECT_ID through it instead of process.env directly, and pass the same resolved environment into loadProjectConfig so the .env files aren't parsed twice. Falls back to process.env directly when no supabase/config.toml exists anywhere (loadProjectEnvironment resolves to null in that case), preserving the existing ambient-only behavior for that scenario. The identical gap in LegacyCliConfig.projectId (used by several other already-ported commands) is out of scope here and left for a follow-up.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fb101635cd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…(review: #5765) Go's Config.Validate parses studio.api_url with net/url.Parse right after the studio.port check, still inside if c.Studio.Enabled (pkg/config/config.go:1074-1078), rejecting malformed hosts such as an unterminated IPv6 literal (http://[::1). legacyResolveLocalConfigValues never checked studio.api_url at all, so native status/default stop could accept a config Go rejects. Extends legacy-storage-url.ts's legacyGoUrlParse (previously Scheme/Path only, since storage ss:// URLs never have a host) with a faithful port of net/url.parseHost's bracket/port validation, verified byte-for-byte against the real Go stdlib.
…r (review: #5765) Go's Config.Validate rejects [auth.passkey] enabled = true configured without a complete [auth.webauthn] (rp_id/rp_origins), right after the signing-keys read and before Auth.Hook.validate() (pkg/config/config.go:1117-1129). legacyResolveLocalConfigValues never read this at all, so native status/default stop could continue for a config Go refuses. @supabase/config's auth schema has no passkey/webauthn fields, so this threads LoadedProjectConfig.document (the raw, pre-schema-default TOML) through legacyResolveLocalConfigValues -> legacyResolveStatusLocalState -> status.handler.ts/stop.handler.ts, the same document-based approach already used for the equivalent check on the db/migration config-load path (legacy-db-config.toml-read.ts's legacyValidateAuthConfig, A6).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6b8d4e9f73
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| projectEnvValues: Readonly<Record<string, string>> | undefined, | ||
| ): number { | ||
| const value = envOverride(name, undefined, projectEnvValues); | ||
| if (value === undefined) return configuredPort; |
There was a problem hiding this comment.
Validate configured ports before returning them
The current helper still returns the decoded configuredPort unchecked whenever there is no direct SUPABASE_*_PORT override, and the shared config schema declares ports as plain numbers. When the value comes from config.toml or an env() substitution, such as db.port = 70000 or -1, Go rejects it while decoding the uint16 field, but this path can continue and emit URLs with an invalid port; apply the same integer/range validation to the configured value too.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
This is the same claim already raised and addressed on this PR (see the resolved discussion on envOverridePort, same file/function): Go's db.port/api.port/etc. decode as uint16 via viper.UnmarshalExact → mapstructure's decodeUint. For a literal TOML integer like 70000, that decode does not hard-fail — reflect.Value.SetUint's truncating conversion silently wraps it to 4464 (70000 mod 65536), and Go proceeds with the wrapped value (verified by building and running that decoder against the identical input). The hard-fail path only exists for the string-sourced SUPABASE_*_PORT env-var override, which goes through mapstructure's WeaklyTypedInput string branch (strconv.ParseUint(str, 0, 16)) — exactly the case envOverridePort's existing MAX_PORT check already covers.
Adding range validation to the plain configuredPort value (as proposed here) would make native status/stop reject literal TOML ports that the real Go CLI silently accepts (with wraparound) — a step away from parity, not toward it. Not making this change, for the same reason as before.
| new LegacyStatusConfigLoadError({ message: `failed to read config: ${String(cause)}` }), | ||
| }); | ||
|
|
||
| const loaded = yield* loadProjectConfig(cliConfig.workdir, { |
There was a problem hiding this comment.
Gate auth subvalidators on auth.enabled
When [auth] enabled = false, Go skips Auth.Sms.validate() and Auth.External.validate() entirely, so stale nested blocks such as [auth.sms.twilio] enabled = true without credentials or [auth.external.github] enabled = true without a secret do not block status/default stop. This load goes through @supabase/config, whose nested requiredWhenEnabled checks run without the parent auth.enabled gate, so native status can reject a disabled auth configuration that the Go CLI accepts; defer those auth sub-section checks to the post-load Go-parity validator.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed as a real gap, not a no-op: Go's Config.Validate nests Auth.Sms.validate()/Auth.External.validate() entirely inside if c.Auth.Enabled (apps/cli-go/pkg/config/config.go:1087,1145,1148), so a disabled auth section with a stale enabled-but-uncredentialed [auth.sms.twilio]/[auth.external.github] block validates fine in Go. In @supabase/config, requiredWhenEnabled (packages/config/src/auth/sms.ts, packages/config/src/auth/providers.ts) only checks each nested provider's own enabled flag, never the parent auth.enabled, so the schema rejects that same config unconditionally during decode — this repo's own legacy-local-config-values.ts:1229-1232 doc comment already flags Sms/External as the two Go-gated sub-checks not yet ported into the (correctly authEnabled-gated) post-load validator.
Holding off on making this change in this run, though: the only place that actually stops the unconditional check from firing is @supabase/config's schema itself (sms.ts/providers.ts), since a nested struct's own .check() fails during decode before any caller-side post-load validator gets a chance to run. That schema is shared by every native command that calls loadProjectConfig — config push, gen types, seed buckets, functions new, functions serve/deploy, not just status/stop — so removing the check there and re-adding it only in status/stop's own legacy-local-config-values.ts would silently drop this validation for every other command, including the auth.enabled=true case those commands correctly reject today. The parity-correct fix is to relocate the check to a cross-field .check() on the top-level auth struct (gated on auth.enabled, still enforced for every caller), not a status/stop-local change — that's real surgery on shared, widely-consumed schema code well beyond this port's stated scope, and I'd rather not make it unilaterally without the coverage/verification pass across every other loadProjectConfig consumer it deserves.
Flagging for @Coly010's call: happy to take this on as a follow-up against @supabase/config if you'd like it tracked separately, or to attempt it inline here if you'd rather have it in this PR.
| new LegacyStatusConfigLoadError({ message: `failed to read config: ${String(cause)}` }), | ||
| ), | ||
| ); | ||
| const config = loaded?.config ?? Schema.decodeUnknownSync(ProjectConfigSchema)({}); |
There was a problem hiding this comment.
Reject TS-only function env blocks
The shared config schema accepts [functions.<slug>.env] records, but the Go function config struct has no env field and v.UnmarshalExact rejects that TOML before status or default stop reaches Docker. By trusting this decoded ProjectConfig without an exact Go-shape check, native status can proceed for a config the Go CLI refuses; add a raw-document validation for function keys that are not in the Go struct before continuing.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Thanks for digging into this — you're right about Go's behavior: v.UnmarshalExact (apps/cli-go/pkg/config/config.go:749) does hard-fail on an unknown key like [functions.<slug>.env] (Go's function struct has no env field, config.go:290-296). I reproduced this directly against the pinned viper/mapstructure versions and confirmed Go rejects it with 'functions[foo]' has invalid keys: env.
That said, env isn't an accidental leftover in @supabase/config's schema — it's the intentional per-function allowlist for the newer environments/config.json system (docs/environments-management.md), and the permissive decode (no Go-struct-exactness check on unknown keys) is a property of @supabase/config shared by every legacy command that calls loadProjectConfig — stop, config push, secrets set, gen types, functions new, etc. all go through the identical tomlOnly fallback path and none of them do Go-shape validation either. Fixing this only in status.handler.ts would leave the identical gap open in all of those and produce inconsistent strictness rather than real parity.
The right fix is a TOML-vs-JSON-aware "reject keys Go's struct doesn't have" guard centralized in packages/config, applied uniformly wherever a tomlOnly load happens — not a per-command patch, and given it needs to distinguish the TOML-only Go-compat path from the JSON/config.json path (which legitimately allows env), it's more involved than this thread's scope. Not making a status/stop-local change here; happy to see this tracked as a follow-up against @supabase/config instead.
…NALYTICS_BACKEND overrides (review: #5765) Two Go-parity gaps in the status/stop config resolver: - Go defaults `auth.email.smtp.enabled = true` when `[auth.email.smtp]` is present but omits `enabled` (pkg/config/config.go:743-748), a presence-based default set on the raw viper map before the struct decodes. The shared schema always decodes a missing `enabled` key to `false`, erasing that presence signal, so this reads the raw TOML document instead (same approach already used for passkey/webauthn and experimental.webhooks). Mirrors the equivalent, already-correct check on the db/migration config-load path (legacy-db-config.toml-read.ts). - `SUPABASE_ANALYTICS_BACKEND` env overrides weren't validated against Go's `LogflareBackend` enum (config.go:60-65), so a bad override (e.g. "mysql") would continue where Go's `UnmarshalText` hard-fails config load. Validates the override-or-configured value with a single check, matching how viper merges both sources into one string before UnmarshalExact runs UnmarshalText exactly once.
…chema decode (review: #5765) Go's `(e external) validate()` (pkg/config/config.go:1418-1423) unconditionally deletes the deprecated `linkedin`/`slack` provider keys before the required-field loop runs, warning on stderr only when the deleted provider was enabled — a bare `[auth.external.slack] enabled = true` with no client_id/secret loads fine in Go. The shared schema still modeled `slack` as a fully-required provider (and had no `slack_oidc` entry at all, unlike `linkedin_oidc`), so `loadProjectConfig` rejected a config Go accepts. This affected every command that loads project config (status, stop, push, pull), not just status/stop. Adds `normalizeDeprecatedExternalProviders` in io.ts, mirroring the existing `normalizeDeprecatedSMTPSections`/`[inbucket]` precedent, and renames the `slack` provider to `slack_oidc` in the schema (config-sync/auth.sync.ts already had unreachable `slack_oidc` cases, confirming this was the intended id). Runs on the post-remote-merge document, not pre-merge like the SMTP normalizer, since Go's external.validate() runs once on the final effective config rather than eagerly over every [remotes.*] entry.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 46916aac3d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ); | ||
| } | ||
|
|
||
| const config = yield* parseProjectConfig(normalizedForDecode, format, filePath); |
There was a problem hiding this comment.
Avoid validating inactive remote subtrees
When no projectRef is selected, remotes is still present in normalizedForDecode, so ProjectConfigSchema validates every nested remote block. That rejects configs Go accepts, e.g. an unselected [remotes.prod.auth.external.github] enabled = true stub without a secret, because Go's Config.Validate only checks remotes.*.project_id for remote blocks unless one is merged into the effective base config; status/default stop can therefore fail before Docker on a remote subtree Go ignores.
Useful? React with 👍 / 👎.
| // `Auth.Email.validate()`, then (skipping the unported Sms/External steps) | ||
| // `Auth.ThirdParty.validate()`, all right after the signing-keys read and | ||
| // still inside `if c.Auth.Enabled` (`pkg/config/config.go:1117-1153`). | ||
| // Sms/External (`config.go:1145-1150`) aren't ported. |
There was a problem hiding this comment.
Validate unknown auth.external providers
When auth is enabled and config.toml declares an arbitrary OAuth provider such as [auth.external.custom] enabled = true without client_id/secret, Go decodes auth.external as a map and validates every enabled provider, so config loading fails before Docker access. The fixed TS schema drops provider keys it does not model and this block explicitly skips External, letting native status/default stop accept configs the Go CLI rejects; validate enabled raw auth.external.* entries under the same authEnabled gate.
Useful? React with 👍 / 👎.
…rify (review: #PRRT_kwDOErm0O86OqN9V)
Go's jwkToRSAPrivateKey (pkg/config/apikeys.go:132-168) builds the RSA
private key from n/e/d/p/q alone and its stdlib derives dp/dq/qi lazily
when absent, so a signing-keys JWK missing those CRT exponents still
signs successfully in Go. Node's createPrivateKey({ format: "jwk" })
has no such fallback and hard-rejects the same key, so native status
failed to generate local anon/service keys for a file the Go CLI
accepts. Compute the missing dp/dq/qi via BigInt before handing the
JWK to Node, matching Go's derivation.
…ew: #PRRT_kwDOErm0O86OqN9Z) Go's LoadEnvHook is the first mapstructure decode hook composed into UnmarshalExact (config.go:749-753,769-772), so it resolves a nested env(VAR) reference on any string mapstructure decodes into the struct, including a SUPABASE_* AutomaticEnv override string itself, not just a config.toml literal. envOverride returned that raw override string verbatim, so e.g. SUPABASE_API_ENABLED=env(API_ENABLED) was rejected as a malformed bool instead of resolving to the referenced value. Resolve one level of env(VAR) indirection with the same projectEnvValues/ process.env precedence and non-empty gate as the outer lookup, reusing packages/config's ENV_CAPTURE_REGEX (now re-exported) rather than duplicating the pattern.
…iew: #PRRT_kwDOErm0O86OrVR-, #PRRT_kwDOErm0O86OrzAH) Go's loadFromFile duplicate-check/selection loop over [remotes.*] reads viper's raw string values (config.go:596-610) and only resolves env(...) afterward, inside the struct decode's LoadEnvHook (config.go:611,749-753, decode_hooks.go:13-26). loadProjectConfigFile instead ran the match/merge against the already-interpolated document, so a [remotes.x] project_id = "env(REF)" would match a caller-supplied, already-resolved REF even though Go compares the literal env(REF) string and never selects that remote. Run the duplicate-check and match/merge against the raw, pre-interpolation document, then interpolate the merged result for the final decode. The project_id FORMAT check (Go's post-decode Config.Validate) still uses the interpolated remotes, since that check genuinely runs after env() resolution in Go.
… #PRRT_kwDOErm0O86OrVSC) Go's Docker.DaemonHost() (internal/utils/docker.go:41-54) comes from a client built via cli.Initialize(), whose endpoint resolution walks DOCKER_HOST -> DOCKER_CONTEXT -> the config file's currentContext -> the context store (docker/cli's getDockerEndPoint/resolveContextName) - not just DOCKER_HOST. legacyGetHostname only checked SUPABASE_SERVICES_HOSTNAME/DOCKER_HOST and fell back to 127.0.0.1, so with an active non-default Docker context and no DOCKER_HOST set, native status could correctly inspect a remote daemon (the docker/ podman binary its ps/inspect calls shell out to already honors the context) while printing unusable localhost API/DB/Studio URLs for it. Resolve the active context's tcp:// endpoint from $DOCKER_CONFIG/config.json and the context store's meta.json before falling back to the loopback default, mirroring Go's precedence.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b15c7dd935
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| Command.withHandler((flags) => | ||
| legacyStop(flags).pipe(withLegacyCommandInstrumentation({ flags }), withJsonErrorHandling), |
There was a problem hiding this comment.
Reject unsupported output formats before stopping
When supabase stop -o csv or -o table is used, the TS global flag parser accepts those db-query-only values and this command proceeds into legacyStop, so containers can be stopped even though the Go CLI would reject the flag before stop.RunE runs. I checked the Go wiring: cmd/root.go:330 registers the root --output enum, and internal/utils/output.go:30-37 allows only env|pretty|json|toml|yaml; table|csv are not valid for stop. Please reject unsupported LegacyOutputFlag values for this native command before invoking the handler.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Thanks for flagging this, but I dug into it and it's already handled — just not visibly in this file. stop.command.ts:62 wraps the handler with withLegacyCommandInstrumentation({ flags }), and that wrapper defaults outputFormats to LEGACY_RESOURCE_OUTPUT_FORMATS (env|pretty|json|toml|yaml, see legacy-go-output-flag.ts) whenever a command doesn't override it. It runs legacyValidateOutputFormat before the handler executes, the same mechanism status and other resource commands use to compensate for Effect CLI's single shared --output enum, since we can't register a per-command EnumFlag the way Go's cmd/root.go:330/internal/utils/output.go:30-37 does.
I confirmed on this branch that stop -o csv and stop -o table both fail immediately with invalid argument "csv" for "-o, --output" flag: must be one of [ env | pretty | json | toml | yaml ] and exit 1 — no Docker calls happen, matching Go's pre-RunE pflag rejection. So no code change is needed here.
I did find that stop's SIDE_EFFECTS.md had a stale/incorrect note claiming Go has no -o/--output flag at all and that TS accepting an unknown value was a "harmless divergence" — that's backwards (Go's -o is a root PersistentFlags() enum every subcommand inherits, and TS already rejects the same values). Fixed that doc in 3c6d766.
| if (goFmt === "pretty") { | ||
| yield* renderPretty(); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Reject db-only output formats before status
When supabase status -o csv or -o table is used, the TS root parser accepts these db-query-only values, but this branch does not reject them and falls through to the normal pretty/status path after config loading and Docker inspection. In Go, the root --output enum registered at apps/cli-go/cmd/root.go:330 only allows env|pretty|json|toml|yaml (apps/cli-go/internal/utils/output.go:30-37), so those invocations fail during flag parsing before status.Run touches Docker; add the same command-level rejection here for unsupported LegacyOutputFlag values.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Thanks for flagging this, but I don't think there's a gap here — status -o csv/-o table are already rejected before status.handler.ts runs any Docker/config logic. The rejection lives one layer up, in status.command.ts, which passes outputFormats: LEGACY_RESOURCE_OUTPUT_FORMATS (env|pretty|json|toml|yaml, matching Go's root --output enum at cmd/root.go:330/internal/utils/output.go:30-37) to withLegacyCommandInstrumentation. That wrapper validates the flag and fails before invoking the handler effect at all — the same way Go's cobra rejects an out-of-enum -o at flag-parse time, before RunE.
I confirmed this by running status -o csv locally — it prints invalid argument "csv" for "-o, --output" flag: must be one of [ env | pretty | json | toml | yaml ] and exits without touching Docker, byte-identical to what Go would say. table/csv are only valid for db query's own separate -o enum (cmd/db.go), which shadows the root persistent flag in cobra — status was never meant to accept them, and it doesn't. No code change needed; adding a second inline check in the handler would just duplicate validation this codebase intentionally centralizes in one place.
The companion stop thread raised the identical claim and turned out to be the same false positive (stop.command.ts inherits the same default gate) — replied there separately, and also fixed a stale doc note in stop's SIDE_EFFECTS.md that this investigation surfaced.
| const loaded = yield* loadProjectConfig(cliConfig.workdir, { | ||
| projectEnv: projectEnv !== null ? { ...projectEnv, values: projectEnvValues } : undefined, |
There was a problem hiding this comment.
Mirror Go's SMS provider switch
When auth is enabled and multiple SMS providers are present, Go's sms.validate() uses a switch over providers, so if [auth.sms.twilio] is enabled and complete, a later incomplete [auth.sms.messagebird] enabled = true block is not inspected and status/default stop continue. This load path decodes through @supabase/config, whose per-provider requiredWhenEnabled checks validate every enabled SMS provider, so native status and default stop can reject a stale secondary SMS block that the Go CLI accepts before reaching Docker.
Useful? React with 👍 / 👎.
…o (review: #PRRT_kwDOErm0O86OrzAK) Go's loadDefaultEnv (apps/cli-go/pkg/config/config.go:1243-1250) omits .env.local from its candidate dotenv list whenever SUPABASE_ENV=test, so a malformed or intentionally non-test supabase/.env.local is invisible to Go in that mode. status/stop's preliminary loadProjectEnvironment call had no such gate and its parse failure propagates as a real command failure, unlike the later legacyResolveProjectEnvironmentValues pass which already re-derives the file list per SUPABASE_ENV. Add an opt-in skipEnvLocal option to loadProjectEnvironment (default off, so next/ and secrets set are unaffected) and pass it from both status.handler.ts and stop.handler.ts when SUPABASE_ENV=test.
…ields (review: #PRRT_kwDOErm0O86OrzAR)
Go's newDecodeHook (apps/cli-go/pkg/config/config.go:775-784) wires
mapstructure.StringToSliceHookFunc(",") unconditionally into config
decode, so a plain string value for a []string field (e.g.
auth.additional_redirect_urls = "http://a,http://b") decodes fine in
Go, whether the string is a literal TOML value or an env()-substituted
one. @supabase/config's schema had no equivalent and rejected any
string value for an array-typed field outright.
Extend the existing pre-decode env() coercion walker
(interpolateEnvReferencesAgainstSchema) with an "array" ExpectedType
for homogeneous string arrays, and apply it to any string leaf at that
path (not just env()-substituted ones, since Go's hook fires
regardless of value source) by splitting on "," -- matching Go's
StringToSliceHookFunc exactly, including its empty-string-to-empty-
slice case.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 09e218a2e4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
PR #5765 wired four Go/viper-parity behaviors unconditionally into the shared packages/config load pipeline, leaking Go-only semantics into next/, packages/stack, and the functions manifest. Gate all four behind a new opt-in LoadProjectConfigOptions.goViperCompat (default false), mirroring the existing tomlOnly/skipEnvLocal precedents: 1. Unconditional [remotes.*] duplicate-project_id/format checks, even without a projectRef (Go: config.go:594-602,996-1001). 2. The deprecated auth.external.{linkedin,slack} WARN on stderr (config.go:1418-1423) — the strip itself stays unconditional. 3. The widened env(...) reference pattern (case-agnostic) vs. the strict SCREAMING_SNAKE_CASE pre-PR pattern. 4. Comma-split coercion of a plain string into a []string field, not just an env()-substituted one (mapstructure.StringToSliceHookFunc). Default-off restores pre-#5765 behavior for every consumer that doesn't opt in. Only the Go-parity legacy shell sets it to true.
…ntics The legacy shell is a strict 1:1 Go CLI port, so every loadProjectConfig/ resolveProjectSubtree/resolveProjectValue call site it owns now sets goViperCompat: true, restoring the four Go/viper decode+validation behaviors gated behind that flag in packages/config. Threads goViperCompat through the two shared functions modules (shared/functions/deploy.ts, shared/functions/serve.ts) as a required dependency field so each shell's handler makes an explicit choice: legacy sets it true, next/ sets it false. Adds regression coverage pinning next/'s default-off behavior (functions dev config, next start's config-load path) and legacy's Go-parity-on behavior (status/stop duplicate and malformed [remotes.*] handling, comma-split array decoding, deprecated-provider stderr warning).
…g.Validate parity Relocates the 5 shared Go-parity regex patterns (bucket name, function slug, hook secret, clerk domain, project ref) and legacyParseGoBool out of legacy-db-config.toml-read.ts into a new legacy-config-validate.ts, verbatim and with no behavior change. legacy-local-config-values.ts's existing import of the same symbols is repointed at the new module. This is a purely mechanical move: it establishes the single home the Config.Validate validator itself will land in during a follow-up commit, per apps/cli/AGENTS.md's "Hoist Before You Duplicate" policy.
…ig-validate Implements legacyValidateResolvedConfig in legacy-config-validate.ts, porting every Config.Validate branch L (legacy-local-config-values.ts) previously duplicated inline (storage buckets, studio, local_smtp, auth site_url/captcha/ passkey/hooks/mfa/smtp/third_party, function slugs, deno_version, analytics, experimental) into the single shared home, plus the 3 pure I/O helpers for signing keys, api.tls cert/key, and email template/notification content path resolution. legacyResolveLocalConfigValues now accumulates inputs across its existing value-derivation order and calls the shared validator once at the end; the 3 file reads stay inline at their original position. D (legacy-db-config.toml-read.ts) is unchanged and still wired up in a follow-up commit.
…ng db.major_version=0 parity legacyReadDbToml now builds a LegacyConfigValidationInput/LegacyAuthInput adapter and runs every Go Config.Validate branch it shares with legacy-local-config-values.ts (db port/major_version, storage bucket names, function slugs, edge_runtime.deno_version, analytics backend/bigquery fields, experimental.pgdelta.format_options, and the auth site_url/captcha/passkey/hooks/mfa/email-smtp/third_party sequence) through the single shared legacyValidateResolvedConfig call, instead of duplicating that logic inline. This fixes the db.major_version = 0 divergence: it previously fell through to the generic "Failed reading config: Invalid db.major_version: 0." message instead of Go's "Missing required field in config: db.major_version". auth.sms and auth.external stay 100% inline in D (never part of the shared validator, per legacy-config-validate.ts's module header) and now run after the shared call succeeds, still gated on auth.enabled.
…y check Move every pure legacyValidateResolvedConfig behavioral test (db.major_version, storage.buckets, edge_runtime.deno_version, analytics, experimental.*, auth.site_url/ captcha/passkey/hooks/mfa/smtp/third_party, functions.*, api.tls presence) out of legacy-local-config-values.unit.test.ts and into legacy-config-validate.unit.test.ts as direct legacyValidateResolvedConfig calls, since both D and L now share that single validator and no longer need per-caller duplication of this coverage. Env-override mechanics, file I/O, and ProjectConfig-schema-specific behavior stay in legacy-local-config-values.unit.test.ts unchanged. Add direct regression coverage for the db.major_version=0 fix (0c62a91) and the auth.captcha.provider decode-time enum check at the shared-validator level, a new test for L's auth.email.template content/content_path exclusivity divergence gained in 49fc64a, and a dedicated legacy-config-validate.parity.unit.test.ts that drives both D's and L's real pipelines with equivalent misconfigurations and asserts they fail with the same Go-parity error message. Also updates legacy-config-validate.ts's stale module-header status note now that both D and L are fully wired through it.
…us-commands # Conflicts: # apps/cli/docs/go-cli-porting-status.md # apps/cli/src/legacy/commands/seed/buckets/buckets.handler.ts
…ets hoist merge develop's db push/reset/start port (#5715) hoisted the seed-buckets config load out of buckets.handler.ts and into the shared legacySeedBucketsRun (also used by db reset --local), which superseded the inline goViperCompat opt-in added on this branch. Carry the opt-in over to the new call site so seed buckets and db reset --local keep Go-parity config semantics.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2ddbeb555a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const hook = config.auth.hook[hookType]; | ||
| if (hook.enabled) { | ||
| hooks.push({ type: hookType, uri: hook.uri ?? "", secrets: hook.secrets ?? "" }); |
There was a problem hiding this comment.
Apply auth hook env overrides before validation
When auth is enabled and a nested override such as SUPABASE_AUTH_HOOK_SEND_EMAIL_ENABLED=true is supplied from the shell or dotenv without a matching URI, Go applies it through Viper ExperimentalBindStruct/AutomaticEnv before Config.Validate, so hook.validate fails with the missing auth.hook.send_email.uri before status or the default stop path reaches Docker. This loop only inspects the TOML-decoded config.auth.hook[...], so an env-only enabled hook remains disabled and native status/stop can accept a config the Go CLI rejects; apply the nested SUPABASE_AUTH_HOOK_* overrides before building the hook validation inputs.
Useful? React with 👍 / 👎.
| enabled: config.auth.captcha.enabled ?? false, | ||
| provider: config.auth.captcha.provider, | ||
| secret: config.auth.captcha.secret, |
There was a problem hiding this comment.
Apply CAPTCHA env overrides before validation
When auth is enabled and a config has [auth.captcha] enabled = false, an environment override like SUPABASE_AUTH_CAPTCHA_ENABLED=true makes Go validate that same block and fail if provider or secret is missing before status/default stop touches Docker. This input is built directly from the decoded TOML values, so the override is ignored and the disabled CAPTCHA block is accepted; resolve SUPABASE_AUTH_CAPTCHA_* before passing the CAPTCHA fields into the Go-parity validator.
Useful? React with 👍 / 👎.
| const passkeyDoc = asRecord(authDocument?.["passkey"]); | ||
| const webauthnDoc = asRecord(authDocument?.["webauthn"]); | ||
| const passkey: LegacyPasskeyInput | undefined = | ||
| passkeyDoc?.["enabled"] === true |
There was a problem hiding this comment.
Apply passkey env overrides before validation
When auth is enabled and [auth.passkey] enabled = false is overridden by SUPABASE_AUTH_PASSKEY_ENABLED=true, Go validates the now-enabled passkey config and rejects it if the required [auth.webauthn] fields are absent. This branch only checks the raw TOML document's enabled value, so the env override is missed and native status/default stop can continue with a config Go rejects; apply the SUPABASE_AUTH_PASSKEY_*/SUPABASE_AUTH_WEBAUTHN_* overrides before building passkey.
Useful? React with 👍 / 👎.
| enabled: smtpDoc["enabled"] === undefined ? true : smtpDoc["enabled"] === true, | ||
| host: typeof smtpDoc["host"] === "string" ? smtpDoc["host"] : "", | ||
| port: typeof smtpDoc["port"] === "number" ? smtpDoc["port"] : 0, | ||
| user: typeof smtpDoc["user"] === "string" ? smtpDoc["user"] : "", |
There was a problem hiding this comment.
Apply SMTP env overrides before validation
When [auth.email.smtp] enabled = false is present and the shell or dotenv sets SUPABASE_AUTH_EMAIL_SMTP_ENABLED=true without the required SMTP fields, Go applies that override before Auth.Email.validate and fails config loading. This code derives enabled and the SMTP fields only from smtpDoc, so native status/default stop can accept the explicitly disabled TOML block even though Go sees it as enabled; include the nested SUPABASE_AUTH_EMAIL_SMTP_* overrides in this input.
Useful? React with 👍 / 👎.
| } | ||
| } | ||
|
|
||
| const config = yield* parseProjectConfig(normalizedForDecode, format, filePath); |
There was a problem hiding this comment.
Reject unknown Go config keys before decode
When goViperCompat callers load a config.toml with an unknown Go key such as a stray top-level foo = "bar" or an unsupported nested field, Go's v.UnmarshalExact rejects the file before status or the default stop path reaches Docker. This decode path uses ProjectConfigSchema, which this file notes ignores excess properties, so those keys are silently dropped and the native commands can run with a config the Go CLI refuses; add an exact raw-document key check for Go-parity loads before parsing into the shared schema.
Useful? React with 👍 / 👎.
| // block and the functions loop (`pkg/config/config.go:1158-1173`), and — | ||
| // unlike `studio.port`/`local_smtp.port` above — unconditionally, with no | ||
| // `edge_runtime.enabled` gate. | ||
| const denoVersion = envOverrideDenoVersion(config.edge_runtime.deno_version, projectEnvValues); |
There was a problem hiding this comment.
Apply remaining typed env overrides before validation
When SUPABASE_EDGE_RUNTIME_POLICY=bogus or SUPABASE_ANALYTICS_PORT=not-a-port is supplied from the shell or dotenv, Go applies it through Viper ExperimentalBindStruct/AutomaticEnv before UnmarshalExact, so the RequestPolicy enum or uint16 port decode fails before status/default stop reaches Docker. This resolver only applies the edge_runtime enabled/deno_version and analytics enabled/backend/GCP overrides, so those remaining typed SUPABASE_* overrides are ignored and native commands can accept configs Go refuses; validate all typed env-overridden fields, even ones status does not render.
Useful? React with 👍 / 👎.
…us-commands # Conflicts: # apps/cli/src/legacy/commands/secrets/set/set.handler.ts # packages/config/src/io.ts
…e quality) Collapses a .pipe() call back to oxfmt's canonical single-line form; introduced by the develop merge conflict resolution and left unformatted.
…ting scheme (review: #PRRT_kwDOErm0O86OsZMu) Go's hookConfig.validate calls url.Parse before the scheme switch (pkg/config/config.go:1497-1499) and rejects a malformed URI (e.g. an unterminated IPv6 host like http://[::1) before status/stop reach Docker. The TS validator only regex-extracted a scheme prefix, so any http:/https: prefix was accepted regardless of the rest of the URI. Reuses the existing legacyGoUrlParse port (already used for studio.api_url) instead of the scheme regex.
…PI keys (review: #PRRT_kwDOErm0O86OrzAU) Go's config.Secret fields (auth.jwt_secret/publishable_key/secret_key/ anon_key/service_role_key, pkg/config/auth.go:181-185) are decrypted by DecryptSecretHookFunc unconditionally during UnmarshalExact (pkg/config/secret.go:30-73), failing config load on an undecryptable value before status/stop continue. @supabase/config's schema only tags these fields for later Redacted wrapping and never decrypts them, so the status/stop resolver was using an "encrypted:..." value as literal (wrong) key material, and never failed on an undecryptable one. Reuses the existing eciesjs-based decrypt primitives from legacy-vault-decrypt.ts (already used by the db/migration config loader for the same Go hook).
…ection (review: #PRRT_kwDOErm0O86Ossk3) Go's -o/--output is a root PersistentFlags() enum (cmd/root.go:330) every subcommand inherits, so "stop -o csv"/"-o table" is rejected by pflag at parse time even though stop.RunE never reads the value itself. The doc claimed Go has no such flag and that TS accepting an unknown value was a harmless divergence — in fact stop.command.ts already rejects the same values via withLegacyCommandInstrumentation's default outputFormats gate, matching Go exactly. No behavior change, just correcting the doc.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3c6d766f35
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const loadedConfig = yield* loadProjectConfig(runtimeInfo.cwd, { | ||
| projectRef: ref, | ||
| goViperCompat: true, | ||
| }).pipe( |
There was a problem hiding this comment.
Swallow invalid remote config errors for secrets set
When config.toml contains a malformed [remotes.*].project_id, enabling goViperCompat here makes loadProjectConfig throw InvalidRemoteProjectIdError, but the tolerant secrets set path only catches parse/env/duplicate errors below. Go’s internal/secrets/set/set.go logs any flags.LoadConfig error and still uploads secrets supplied via --env-file or positional NAME=value, so this can now abort a valid secret update before the explicit secrets are sent; handle the invalid-remote error the same way as the duplicate-remote error.
Useful? React with 👍 / 👎.
| const failedStop = stopResults.find( | ||
| (result) => Result.isFailure(result) || result.success !== 0, | ||
| ); |
There was a problem hiding this comment.
Ignore already-stopped containers during stop
When a labeled Supabase container is already exited (for example after a prior crash or manual docker stop), this treats any non-zero docker stop <id> result as fatal and returns before the prune steps. The Go implementation lists All: true too, but explicitly ignores Docker's errdefs.IsNotModified from ContainerStop, so the same stale stopped container is pruned successfully; ignore the CLI's not-running failure instead of aborting the whole stop.
Useful? React with 👍 / 👎.
| const { document: normalizedForDecode, deprecatedProviders } = | ||
| normalizeDeprecatedExternalProviders(documentForDecode); |
There was a problem hiding this comment.
Preserve Slack provider unless Go parity is requested
When a non-Go caller loads a config containing the previously supported [auth.external.slack] block without goViperCompat, this unconditional normalization deletes that provider before schema decoding while the warning is gated off below. The new option is documented as preserving pre-PR behavior for next/, packages/stack, and other default callers, but those paths now silently lose the configured Slack provider instead of retaining or warning about it; gate this stripping behind goViperCompat or keep the old field for default loads.
Useful? React with 👍 / 👎.
Summary
Ports
supabase stopandsupabase statusfrom Go-proxy stubs to native TypeScript in the legacy CLI shell (CLI-1324).startis still Go-proxied, so this intentionally does not route through@supabase/stack/effect's daemon-based orchestration model — that substrate manages a different set of containers than the ones Go's binary actually creates, and using it would silently no-op against a real running stack.legacy-docker-lifecycle,legacy-go-jwt,legacy-local-config-values,legacy-api-url) is reused by both commands, matching Go's local-dev defaults exactly — including a Go-byte-exact JWT signer, since@supabase/stack's own JWT generator uses a different issuer/claim order than what Go prints for local dev keys.*.live.test.tsas a documented test category (AGENTS.md) alongside unit/integration/e2e: black-box subprocess tests run by thecli-e2e-ciharness against a real platform.stop/statusdon't call the Management API, so their live tests spin up a real local Docker stack instead and verify against it directly (e.g. confirming Docker itself has no containers left afterstop, not just trusting the CLI's exit code).Notable review findings fixed along the way
stderr's TTY status while writing tostdout— piping stdout while stderr stayed a TTY (supabase status | less) would have corrupted output with ANSI escapes (the same bug class CLI-1546 fixed once before).--override-namewas leaking into pretty-mode output; Go'sPrettyPrintrebuilds a fresh, un-overridden view and ignores it there.--backup/--no-backup: Go's--backupflag is dead code (declared, never bound to a variable incmd/stop.go) — the port now matches that exactly instead of an intended-but-never-true semantic.Post-review hardening: scoping and consolidation
Two structural fixes on top of the port, addressing drift introduced while iterating on review feedback:
next/no longer inherits Go-parity config semantics. The four viper-compat behaviors added during review (unconditional[remotes.*]project-id checks, the deprecated-provider WARN, the widenedenv()reference pattern, and comma-split coercion into array-typed fields) are now gated behind an opt-ingoViperCompatflag onLoadProjectConfigOptions— default off restores the pre-review behavior fornext/,packages/stack, and other non-parity consumers, following the same opt-in pattern astomlOnly/skipEnvLocal. Only legacy-shell callers opt in. Regression tests pin the default-off behavior, including thenext startconfig-load path that would otherwise have started hard-failing on a malformed[remotes.*].project_id.Config.Validatenow has exactly one TS home.legacy/shared/legacy-config-validate.tsreplaces the validation orchestration previously duplicated betweenlegacy-db-config.toml-read.ts(db/migration family) andlegacy-local-config-values.ts(status/stop); both callers build a normalizedLegacyConfigValidationInputfrom their own pipelines and call the shared validator, and a cross-caller parity test feeds identical broken configs through both real pipelines asserting identical error strings. Consolidating surfaced and fixed three real divergences between the two copies:db.major_version = 0now emits Go'sMissing required field in config: db.major_version(the db loader previously emitted a non-Go message), and the captcha provider-enum and emailcontent-vs-content_pathchecks are now present in both paths.CLOSES CLI-1324