From 17c52c3e3be303144c457d3e411a27f53e452be2 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Thu, 8 Oct 2026 13:52:41 +0100 Subject: [PATCH 1/9] refactor(config): rename goViperCompat to cliCompat and centralise CLI loading The Go CLI is gone on next, so the internal loader option and the functions config hook no longer describe Go/viper parity. Rename them and route every apps/cli caller through one wrapper that always enables the CLI's loading semantics, guarded by a code-structure test. No behaviour change. Co-Authored-By: Claude Opus 5.5 --- .../src/command-internal/cli-config-load.ts | 37 +++++++++++ .../src/command-internal/config-pull-run.ts | 20 +++--- ...go-config.ts => functions-local-config.ts} | 4 +- .../command-internal/local-project-context.ts | 3 +- apps/cli/src/command-internal/seed-buckets.ts | 7 +- apps/cli/src/command-internal/stack-config.ts | 3 +- apps/cli/src/commands/config/config.load.ts | 3 +- .../commands/functions/deploy/SIDE_EFFECTS.md | 24 +++---- .../functions/deploy/deploy.handler.ts | 4 +- .../deploy/deploy.integration.test.ts | 6 +- .../functions/download/SIDE_EFFECTS.md | 22 +++---- .../functions/download/download.handler.ts | 4 +- .../download/download.integration.test.ts | 4 +- .../src/commands/functions/new/new.handler.ts | 3 +- .../commands/functions/serve/serve.handler.ts | 5 +- .../functions/serve/serve.integration.test.ts | 5 +- .../commands/gen/gen.signing-keys-config.ts | 3 +- .../src/commands/gen/types/types.handler.ts | 3 +- apps/cli/src/commands/pull/pull.steps.ts | 4 +- .../src/commands/secrets/set/set.handler.ts | 11 ++-- .../seed/buckets/buckets.integration.test.ts | 3 +- apps/cli/src/commands/start/start.handler.ts | 4 +- .../status/status.integration.test.ts | 2 +- .../commands/stop/stop.integration.test.ts | 2 +- .../cli/src/commands/storage/storage.frame.ts | 11 ++-- .../shared/cli/code-structure.unit.test.ts | 33 ++++++++++ apps/cli/src/shared/functions/deploy.ts | 12 ++-- apps/cli/src/shared/functions/download.ts | 8 +-- .../src/shared/functions/functions-config.ts | 14 ++-- apps/cli/src/shared/functions/serve.ts | 53 ++++++--------- ...ull-write-strategy-and-scope-resolution.md | 2 +- packages/config/AGENTS.md | 10 +-- packages/config/README.md | 16 ++--- packages/config/docs/cli-config-loading.md | 10 +-- packages/config/src/config-document.ts | 10 +-- packages/config/src/effect.ts | 4 +- packages/config/src/internal.ts | 2 +- packages/config/src/io.ts | 22 +++---- packages/config/src/io.unit.test.ts | 64 +++++++++---------- packages/config/src/lib/env.ts | 33 ++++------ packages/config/src/lib/resolve.ts | 30 ++++----- packages/config/src/project.ts | 10 +-- packages/config/src/project.unit.test.ts | 4 +- 43 files changed, 285 insertions(+), 249 deletions(-) create mode 100644 apps/cli/src/command-internal/cli-config-load.ts rename apps/cli/src/command-internal/{functions-go-config.ts => functions-local-config.ts} (94%) diff --git a/apps/cli/src/command-internal/cli-config-load.ts b/apps/cli/src/command-internal/cli-config-load.ts new file mode 100644 index 0000000000..55dd5d2c85 --- /dev/null +++ b/apps/cli/src/command-internal/cli-config-load.ts @@ -0,0 +1,37 @@ +import type { CliProjectEnvironment } from "@supabase/config"; +import { + decodeCliConfigDocumentForValidationEffect as decodeDocumentWithCompat, + loadCliConfig as loadConfigWithCompat, + resolveCliConfigSubtree as resolveSubtreeWithCompat, + resolveCliConfigValue as resolveValueWithCompat, + type DecodeCliConfigDocumentForValidationEffectOptions, + type InternalLoadCliConfigOptions, +} from "@supabase/config/internal"; + +export type CliConfigLoadOptions = Omit; +export type CliConfigValidationOptions = Omit< + DecodeCliConfigDocumentForValidationEffectOptions, + "cliCompat" +>; + +/** The single `apps/cli` entrypoint that loads config with the CLI's own loader semantics. */ +export const loadCliConfig = (cwd: string, options?: CliConfigLoadOptions) => + loadConfigWithCompat(cwd, { ...options, cliCompat: true }); + +/** Decodes a pending document with the same semantics as {@link loadCliConfig}. */ +export const decodeCliConfigDocumentForValidation = ( + document: Record, + options: CliConfigValidationOptions, +) => decodeDocumentWithCompat(document, { ...options, cliCompat: true }); + +export const resolveCliConfigValue = ( + value: T, + cliProjectEnv: Pick, + configPath: string, +) => resolveValueWithCompat(value, cliProjectEnv, configPath, { cliCompat: true }); + +export const resolveCliConfigSubtree = ( + value: T, + cliProjectEnv: Pick, + pathPrefix: string, +) => resolveSubtreeWithCompat(value, cliProjectEnv, pathPrefix, { cliCompat: true }); diff --git a/apps/cli/src/command-internal/config-pull-run.ts b/apps/cli/src/command-internal/config-pull-run.ts index 8327855dba..167d3f415b 100644 --- a/apps/cli/src/command-internal/config-pull-run.ts +++ b/apps/cli/src/command-internal/config-pull-run.ts @@ -9,11 +9,9 @@ import { } from "@supabase/config/effect"; import { applyConfigEdits, - decodeCliConfigDocumentForValidationEffect, writeCliConfigDocumentText, type ConfigEdit, type ConfigEditRefusalReason, - type DecodeCliConfigDocumentForValidationEffectOptions, } from "@supabase/config/internal"; import type { ConfigChange } from "@supabase/config"; import { operationDefinitions } from "@supabase/api/effect"; @@ -22,6 +20,10 @@ import { Effect, FileSystem, Result, Schema, SchemaIssue } from "effect"; import { CommandPlatformApi } from "../auth/command-platform-api.service.ts"; import { CommandSettings } from "../config/command-settings.service.ts"; import { Output } from "../shared/output/output.service.ts"; +import { + decodeCliConfigDocumentForValidation, + type CliConfigValidationOptions, +} from "./cli-config-load.ts"; import { sanitizeErrorBody, sanitizeInlineName } from "./http-errors.ts"; import type { ConfigTarget } from "./project-target.ts"; import { BRANCH_UUID_PATTERN } from "./ref-patterns.ts"; @@ -298,7 +300,7 @@ function configPullChangeRelativeValue( } /** - * Converts a failed {@link decodeCliConfigDocumentForValidationEffect} attempt's + * Converts a failed {@link decodeCliConfigDocumentForValidation} attempt's * `SchemaIssue` paths into `ConfigChange.path`-relative segments. `isLabelPrefixed` strips * one leading segment for the raw/unmerged projection of a remote destination, whose issue * paths start with the `remotes` map's own key (the label) rather than already being @@ -372,16 +374,16 @@ function configPullFamiliesForChangePaths( } /** - * Runs {@link decodeCliConfigDocumentForValidationEffect}, capturing only its own + * Runs {@link decodeCliConfigDocumentForValidation}, capturing only its own * `CliConfigParseError` failure into a `Result`. A genuinely malformed `.env`/`.env.local`, * or a filesystem failure reading one, is not a decode-attribution failure, so those * propagate uncaught, matching how the real `loadCliConfig` call already handles them. */ function decodeConfigPullValidation( document: Record, - options: DecodeCliConfigDocumentForValidationEffectOptions, + options: CliConfigValidationOptions, ) { - return decodeCliConfigDocumentForValidationEffect(document, options).pipe( + return decodeCliConfigDocumentForValidation(document, options).pipe( Effect.map(Result.succeed), Effect.catchTag("CliConfigParseError", (cause) => Effect.succeed(Result.fail(cause))), ); @@ -389,7 +391,7 @@ function decodeConfigPullValidation( /** * The change-path keys ({@link configPathKey}) that already fail - * {@link decodeCliConfigDocumentForValidationEffect} in `rawDocument` as it sits on disk + * {@link decodeCliConfigDocumentForValidation} in `rawDocument` as it sits on disk * right now, before this pull's own writes are projected. {@link validateConfigPullPlan} * excludes these from the families it forms — pulling should never attribute, drop, or fail * over a pre-existing problem; it only needs to leave the file no worse than it was. Runs @@ -406,7 +408,6 @@ const configPullPreExistingFailingChangePathKeys = Effect.fnUntraced(function* ( const rawDecoded = yield* decodeConfigPullValidation(input.rawDocument, { path: input.configPath, format: input.format, - goViperCompat: true, }); if (Result.isFailure(rawDecoded)) { for (const path of configPullSchemaIssueChangePaths( @@ -420,7 +421,6 @@ const configPullPreExistingFailingChangePathKeys = Effect.fnUntraced(function* ( const mergedDecoded = yield* decodeConfigPullValidation(input.rawDocument, { path: input.configPath, format: input.format, - goViperCompat: true, remoteName: input.destination.label, }); if (Result.isFailure(mergedDecoded)) { @@ -471,14 +471,12 @@ const validateConfigPullPlan = Effect.fnUntraced(function* (input: { input.projectRef, ); const rawDecoded = yield* decodeConfigPullValidation(document, { - goViperCompat: true, path: input.configPath, format: input.format, }); const mergedDecoded = destination.kind === "remote" ? yield* decodeConfigPullValidation(document, { - goViperCompat: true, path: input.configPath, format: input.format, remoteName: destination.label, diff --git a/apps/cli/src/command-internal/functions-go-config.ts b/apps/cli/src/command-internal/functions-local-config.ts similarity index 94% rename from apps/cli/src/command-internal/functions-go-config.ts rename to apps/cli/src/command-internal/functions-local-config.ts index d107110529..50ff4991eb 100644 --- a/apps/cli/src/command-internal/functions-go-config.ts +++ b/apps/cli/src/command-internal/functions-local-config.ts @@ -1,5 +1,5 @@ import { Effect } from "effect"; -import type { FunctionsGoConfigCompat } from "../shared/functions/functions-config.ts"; +import type { FunctionsLocalConfigLoader } from "../shared/functions/functions-config.ts"; import { loadLocalProjectContext } from "./local-project-context.ts"; import { resolveLocalConfigValues } from "./local-config-values.ts"; @@ -17,7 +17,7 @@ function toError(cause: unknown): Error { * local-dev values (JWTs, URLs) are discarded here; only `projectId`/`edgeRuntimeDenoVersion` and * the validation side effect (throws on the first failure) matter to these three commands. */ -export const functionsGoConfigCompat: FunctionsGoConfigCompat = { +export const functionsLocalConfigLoader: FunctionsLocalConfigLoader = { load: ({ projectRoot, projectRef }) => Effect.gen(function* () { const context = yield* loadLocalProjectContext(projectRoot, toError, projectRef); diff --git a/apps/cli/src/command-internal/local-project-context.ts b/apps/cli/src/command-internal/local-project-context.ts index 98487222a8..989d186fb4 100644 --- a/apps/cli/src/command-internal/local-project-context.ts +++ b/apps/cli/src/command-internal/local-project-context.ts @@ -4,7 +4,7 @@ import { type LoadedCliConfig, type CliConfig, } from "@supabase/config/effect"; -import { loadCliConfig } from "@supabase/config/internal"; +import { loadCliConfig } from "./cli-config-load.ts"; import { Crypto, Effect, FileSystem, Path, Schema } from "effect"; import { recordOrioleDbTelemetry } from "./db-image.ts"; @@ -68,7 +68,6 @@ export const loadLocalProjectContext = ( // Restricts resolution to `supabase/config.toml`; without this, a workdir with a stray // `config.json` would be preferred over it. tomlOnly: true, - goViperCompat: true, projectRef, }).pipe( Effect.mapError((cause) => mapConfigLoadError(`failed to read config: ${String(cause)}`)), diff --git a/apps/cli/src/command-internal/seed-buckets.ts b/apps/cli/src/command-internal/seed-buckets.ts index 0fda1b5e35..23c8228b57 100644 --- a/apps/cli/src/command-internal/seed-buckets.ts +++ b/apps/cli/src/command-internal/seed-buckets.ts @@ -1,5 +1,5 @@ import { type CliConfig, CliConfigSchema } from "@supabase/config/effect"; -import { loadCliConfig, type InternalLoadCliConfigOptions } from "@supabase/config/internal"; +import { loadCliConfig, type CliConfigLoadOptions } from "./cli-config-load.ts"; import { BunPath } from "@effect/platform-bun"; import { Effect, FileSystem, Path, Schema } from "effect"; import { FetchHttpClient } from "effect/unstable/http"; @@ -171,10 +171,7 @@ export const seedBucketsRun = Effect.fnUntraced(function* (opts: { // An explicit `opts.workdir` is the exact project root; only the caller's own workdir // (`cliSettings.workdir`) may still search ancestors for `config.toml`. const search = opts.workdir === undefined && shouldSearchAncestors(cliSettings); - const loadOptions: InternalLoadCliConfigOptions = - projectRef !== "" - ? { projectRef, goViperCompat: true, search } - : { goViperCompat: true, search }; + const loadOptions: CliConfigLoadOptions = projectRef !== "" ? { projectRef, search } : { search }; const loaded = opts.resolvedConfig !== undefined ? null diff --git a/apps/cli/src/command-internal/stack-config.ts b/apps/cli/src/command-internal/stack-config.ts index b8094c3d99..b666ded79f 100644 --- a/apps/cli/src/command-internal/stack-config.ts +++ b/apps/cli/src/command-internal/stack-config.ts @@ -1,5 +1,5 @@ import { getDefaultCliConfig, type CliConfig } from "@supabase/config"; -import { resolveCliConfigSubtree } from "@supabase/config/internal"; +import { resolveCliConfigSubtree } from "./cli-config-load.ts"; import { validateCliConfig } from "@supabase/config/effect"; import { DEFAULT_LOCAL_S3_ACCESS_KEY_ID, @@ -1029,7 +1029,6 @@ export const loadStackConfig = Effect.fn("StackConfig.load")( config.env, { values: context.projectEnvValues }, `functions.${name}.env`, - { goViperCompat: true }, ).pipe( Effect.map( (env) => diff --git a/apps/cli/src/commands/config/config.load.ts b/apps/cli/src/commands/config/config.load.ts index 09f67694ec..e18db7d386 100644 --- a/apps/cli/src/commands/config/config.load.ts +++ b/apps/cli/src/commands/config/config.load.ts @@ -1,4 +1,4 @@ -import { loadCliConfig } from "@supabase/config/internal"; +import { loadCliConfig } from "../../command-internal/cli-config-load.ts"; import { Effect } from "effect"; import { @@ -23,7 +23,6 @@ export function loadLocalConfig( ) { return loadCliConfig(cliSettings.workdir, { projectRef, - goViperCompat: true, search: shouldSearchAncestors(cliSettings), }).pipe( Effect.catchTags({ diff --git a/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md b/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md index 834397acf9..4d4198f137 100644 --- a/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md @@ -2,18 +2,18 @@ ## Files Read -| Path | Format | When | -| -------------------------------------------------------------------------------------------------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `~/.supabase/access-token` | plain text | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | -| `/supabase/{.env,.env.local,.env.,.env..local}` and the same four at the project root | dotenv | project dotenv (`resolveProjectEnvironmentValues`), merged into the `SUPABASE_*` overrides below and threaded into registry resolution | -| `/supabase/config.toml` | TOML | to resolve function config, project id, and local Functions — via `goConfigCompat`'s `tomlOnly: true`/`search: false` (same resolver `start`/`stop`/`status` use), so `config.json` is never read here and no ancestor directory is searched past `` — this now (CLI-2285) applies to the functions manifest inference as well, so the two loads can never disagree about which project they resolve; also runs the full `Config.Validate` pipeline (`resolveLocalConfigValues`), so an invalid config fails up front even for fields this command never otherwise reads | -| `/supabase/`, `[api.tls]` cert/key paths, email template `content_path` — when configured | varies | as part of the `Config.Validate` pipeline above, unconditionally; a `content_path` resolved path is CONFINED to the project root (symlinks dereferenced with `realpathSync`) before it is read, aborting with `resolves outside the project root` before any other work | -| `/supabase/functions//index.ts` | TypeScript | function source to deploy | -| `/supabase/functions/**/deno.json*` | JSON/JSONC | when resolving import maps | -| imported modules | TypeScript | when walking local import graphs for deploy uploads/bundles | -| configured static files | any | when `static_files` patterns match local files | -| `package.json` next to function entrypoint | JSON | Docker bundling package discovery | -| `/supabase/functions/import_map.json` | JSON | deprecated fallback import map discovery | +| Path | Format | When | +| -------------------------------------------------------------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `~/.supabase/access-token` | plain text | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | +| `/supabase/{.env,.env.local,.env.,.env..local}` and the same four at the project root | dotenv | project dotenv (`resolveProjectEnvironmentValues`), merged into the `SUPABASE_*` overrides below and threaded into registry resolution | +| `/supabase/config.toml` | TOML | to resolve function config, project id, and local Functions — via `localConfigLoader`'s `tomlOnly: true`/`search: false` (same resolver `start`/`stop`/`status` use), so `config.json` is never read here and no ancestor directory is searched past `` — this now (CLI-2285) applies to the functions manifest inference as well, so the two loads can never disagree about which project they resolve; also runs the full `Config.Validate` pipeline (`resolveLocalConfigValues`), so an invalid config fails up front even for fields this command never otherwise reads | +| `/supabase/`, `[api.tls]` cert/key paths, email template `content_path` — when configured | varies | as part of the `Config.Validate` pipeline above, unconditionally; a `content_path` resolved path is CONFINED to the project root (symlinks dereferenced with `realpathSync`) before it is read, aborting with `resolves outside the project root` before any other work | +| `/supabase/functions//index.ts` | TypeScript | function source to deploy | +| `/supabase/functions/**/deno.json*` | JSON/JSONC | when resolving import maps | +| imported modules | TypeScript | when walking local import graphs for deploy uploads/bundles | +| configured static files | any | when `static_files` patterns match local files | +| `package.json` next to function entrypoint | JSON | Docker bundling package discovery | +| `/supabase/functions/import_map.json` | JSON | deprecated fallback import map discovery | ## Files Written diff --git a/apps/cli/src/commands/functions/deploy/deploy.handler.ts b/apps/cli/src/commands/functions/deploy/deploy.handler.ts index 30ae2493d6..e65226f204 100644 --- a/apps/cli/src/commands/functions/deploy/deploy.handler.ts +++ b/apps/cli/src/commands/functions/deploy/deploy.handler.ts @@ -2,7 +2,7 @@ import { Effect, Option, Path, Stdio } from "effect"; import { deployFunctions } from "../../../shared/functions/deploy.ts"; import { resolveEdgeRuntimeVersionPin } from "../../../shared/functions/functions.shared.ts"; import { aqua, bold, yellow } from "../../../command-internal/colors.ts"; -import { functionsGoConfigCompat } from "../../../command-internal/functions-go-config.ts"; +import { functionsLocalConfigLoader } from "../../../command-internal/functions-local-config.ts"; import { CommandPlatformApi } from "../../../auth/command-platform-api.service.ts"; import { CommandSettings } from "../../../config/command-settings.service.ts"; import { ProjectRefResolver } from "../../../config/project-ref.service.ts"; @@ -39,7 +39,7 @@ export const functionsDeploy = Effect.fn("functions.deploy")(function* ( projectRoot: cliSettings.workdir, supabaseDir: path.join(cliSettings.workdir, "supabase"), dashboardUrl: dashboardUrl(cliSettings.profile), - goConfigCompat: functionsGoConfigCompat, + localConfigLoader: functionsLocalConfigLoader, yes, rawArgs, edgeRuntimeVersion, diff --git a/apps/cli/src/commands/functions/deploy/deploy.integration.test.ts b/apps/cli/src/commands/functions/deploy/deploy.integration.test.ts index f06ac21d68..f68be729da 100644 --- a/apps/cli/src/commands/functions/deploy/deploy.integration.test.ts +++ b/apps/cli/src/commands/functions/deploy/deploy.integration.test.ts @@ -36,7 +36,7 @@ import { shouldChmodBundleOutputDirectory, } from "../../../shared/functions/deploy.ts"; import { toDockerPath } from "../../../shared/functions/functions-docker.ts"; -import { functionsGoConfigCompat } from "../../../command-internal/functions-go-config.ts"; +import { functionsLocalConfigLoader } from "../../../command-internal/functions-local-config.ts"; import { ConflictingFunctionDeployFlagsError, FunctionImportMapSyntaxError, @@ -1444,7 +1444,7 @@ describe("functions deploy", () => { projectRoot: tempRoot.current, supabaseDir: path.join(tempRoot.current, "supabase"), dashboardUrl: "https://supabase.com/dashboard", - goConfigCompat: functionsGoConfigCompat, + localConfigLoader: functionsLocalConfigLoader, yes: false, rawArgs: ["functions", "deploy"], edgeRuntimeVersion: "1.69.12", @@ -2047,7 +2047,7 @@ describe("functions deploy", () => { projectRoot: tempRoot.current, supabaseDir: path.join(tempRoot.current, "supabase"), dashboardUrl: "https://supabase.com/dashboard", - goConfigCompat: functionsGoConfigCompat, + localConfigLoader: functionsLocalConfigLoader, yes: false, rawArgs: ["functions", "deploy", "hello-world", "--use-api=false"], edgeRuntimeVersion: "1.69.12", diff --git a/apps/cli/src/commands/functions/download/SIDE_EFFECTS.md b/apps/cli/src/commands/functions/download/SIDE_EFFECTS.md index 8b01da46d1..6d118f9452 100644 --- a/apps/cli/src/commands/functions/download/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/functions/download/SIDE_EFFECTS.md @@ -2,17 +2,17 @@ ## Files Read -| Path | Format | When | -| -------------------------------------------------------------------------------------------------------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `/access-token` | plain text | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | -| `/profile` | plain text | when `--profile` and `SUPABASE_PROFILE` are both unset | -| `.yaml` | YAML | when `SUPABASE_PROFILE` or `--profile` points to a file | -| `/supabase/.temp/project-ref` | plain text | when `--project-ref` and `SUPABASE_PROJECT_ID` are both unset | -| `/supabase/.temp/edge-runtime-version` | plain text | Read unconditionally by `resolveEdgeRuntimeVersionPin()` in the handler, before the shared downloader chooses `--use-api` vs Docker — only affects the resolved edge-runtime image tag on the Docker-unbundle path | -| `/supabase/{.env,.env.local,.env.,.env..local}` and the same four at the project root | dotenv | Docker-unbundle path only, before resolving config.toml — project dotenv (`resolveProjectEnvironmentValues`), merged into the `SUPABASE_*` overrides below and threaded into registry resolution | -| `/supabase/config.toml` | TOML | Read unconditionally after resolving the project ref, before checking `--use-api`/`--use-docker` or whether Docker is running — resolves `edge_runtime.deno_version` and `project_id` (`loadCliConfig`) for the Docker-unbundle path. `goViperCompat`'s `tomlOnly: true` means `config.json` is never read here, unlike other `loadCliConfig` callers. A malformed config fails here even on the `--use-api` invocation. Also runs the full `Config.Validate` pipeline (`resolveLocalConfigValues`, same one `start`/`stop`/`status` already use) — an invalid config (bad `db.major_version`, malformed auth hook, etc.) now fails the Docker-unbundle path up front, even for fields this command never otherwise reads. | -| `/supabase/`, `[api.tls]` cert/key paths, email template `content_path` — when configured | varies | Docker-unbundle path only, as part of the `Config.Validate` pipeline above — read even though this command never uses their contents; a `content_path` resolved path is CONFINED to the project root (symlinks dereferenced with `realpathSync`) before it is read, aborting with `resolves outside the project root` before any other work | -| `/telemetry.json` | JSON | when present, before post-run telemetry state is refreshed | +| Path | Format | When | +| -------------------------------------------------------------------------------------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `/access-token` | plain text | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | +| `/profile` | plain text | when `--profile` and `SUPABASE_PROFILE` are both unset | +| `.yaml` | YAML | when `SUPABASE_PROFILE` or `--profile` points to a file | +| `/supabase/.temp/project-ref` | plain text | when `--project-ref` and `SUPABASE_PROJECT_ID` are both unset | +| `/supabase/.temp/edge-runtime-version` | plain text | Read unconditionally by `resolveEdgeRuntimeVersionPin()` in the handler, before the shared downloader chooses `--use-api` vs Docker — only affects the resolved edge-runtime image tag on the Docker-unbundle path | +| `/supabase/{.env,.env.local,.env.,.env..local}` and the same four at the project root | dotenv | Docker-unbundle path only, before resolving config.toml — project dotenv (`resolveProjectEnvironmentValues`), merged into the `SUPABASE_*` overrides below and threaded into registry resolution | +| `/supabase/config.toml` | TOML | Read unconditionally after resolving the project ref, before checking `--use-api`/`--use-docker` or whether Docker is running — resolves `edge_runtime.deno_version` and `project_id` (`loadCliConfig`) for the Docker-unbundle path. The local config loader sets `tomlOnly: true`, so `config.json` is never read here, unlike other `loadCliConfig` callers. A malformed config fails here even on the `--use-api` invocation. Also runs the full `Config.Validate` pipeline (`resolveLocalConfigValues`, same one `start`/`stop`/`status` already use) — an invalid config (bad `db.major_version`, malformed auth hook, etc.) now fails the Docker-unbundle path up front, even for fields this command never otherwise reads. | +| `/supabase/`, `[api.tls]` cert/key paths, email template `content_path` — when configured | varies | Docker-unbundle path only, as part of the `Config.Validate` pipeline above — read even though this command never uses their contents; a `content_path` resolved path is CONFINED to the project root (symlinks dereferenced with `realpathSync`) before it is read, aborting with `resolves outside the project root` before any other work | +| `/telemetry.json` | JSON | when present, before post-run telemetry state is refreshed | ## Files Written diff --git a/apps/cli/src/commands/functions/download/download.handler.ts b/apps/cli/src/commands/functions/download/download.handler.ts index ee0f4a160e..39d93e9bd1 100644 --- a/apps/cli/src/commands/functions/download/download.handler.ts +++ b/apps/cli/src/commands/functions/download/download.handler.ts @@ -3,7 +3,7 @@ import { downloadFunctions } from "../../../shared/functions/download.ts"; import { resolveEdgeRuntimeVersionPin } from "../../../shared/functions/functions.shared.ts"; import { Output } from "../../../shared/output/output.service.ts"; import { aqua, bold, yellow } from "../../../command-internal/colors.ts"; -import { functionsGoConfigCompat } from "../../../command-internal/functions-go-config.ts"; +import { functionsLocalConfigLoader } from "../../../command-internal/functions-local-config.ts"; import { CommandPlatformApi } from "../../../auth/command-platform-api.service.ts"; import { CommandSettings } from "../../../config/command-settings.service.ts"; import { ProjectRefResolver } from "../../../config/project-ref.service.ts"; @@ -33,7 +33,7 @@ export const functionsDownload = Effect.fn("functions.download")(function* ( api, projectRoot: cliSettings.workdir, rawArgs, - goConfigCompat: functionsGoConfigCompat, + localConfigLoader: functionsLocalConfigLoader, edgeRuntimeVersion, // Written to stderr, matching `bold`'s default TTY gate. styleEmphasis: (text) => bold(text), diff --git a/apps/cli/src/commands/functions/download/download.integration.test.ts b/apps/cli/src/commands/functions/download/download.integration.test.ts index 7ed24275f1..71f5a34b2e 100644 --- a/apps/cli/src/commands/functions/download/download.integration.test.ts +++ b/apps/cli/src/commands/functions/download/download.integration.test.ts @@ -38,7 +38,7 @@ import { mockOutput } from "../../../../tests/helpers/mocks.ts"; import { mockChildProcessSpawner } from "../../../../tests/helpers/child-process-spawner.ts"; import { containerRuntimeNotFoundMessage } from "../../../command-internal/container-cli.ts"; import { downloadFunctions } from "../../../shared/functions/download.ts"; -import { functionsGoConfigCompat } from "../../../command-internal/functions-go-config.ts"; +import { functionsLocalConfigLoader } from "../../../command-internal/functions-local-config.ts"; import { CommandPlatformApi } from "../../../auth/command-platform-api.service.ts"; import { ConflictingFunctionDownloadFlagsError } from "../../../shared/functions/download.errors.ts"; import { FunctionsApiStatusError } from "../../../shared/functions/functions-api.errors.ts"; @@ -2277,7 +2277,7 @@ describe("functions download", () => { api: platformApi, projectRoot: tempRoot.current, rawArgs: ["functions", "download", "hello-world", "--project-ref", PROJECT_ID], - goConfigCompat: functionsGoConfigCompat, + localConfigLoader: functionsLocalConfigLoader, edgeRuntimeVersion: "1.69.12", resolveProjectRef: () => Effect.succeed(PROJECT_ID), styleWarning: (text) => `${text}`, diff --git a/apps/cli/src/commands/functions/new/new.handler.ts b/apps/cli/src/commands/functions/new/new.handler.ts index 4e3c23b12a..9bfc8170f2 100644 --- a/apps/cli/src/commands/functions/new/new.handler.ts +++ b/apps/cli/src/commands/functions/new/new.handler.ts @@ -1,4 +1,4 @@ -import { loadCliConfig } from "@supabase/config/internal"; +import { loadCliConfig } from "../../../command-internal/cli-config-load.ts"; import { defaultPublishableKey } from "../../../shared/stack-constants.ts"; import { Effect, FileSystem, Option, Path } from "effect"; @@ -96,7 +96,6 @@ const resolveTemplateInputs = Effect.fn("functions.new.resolveTemplateInputs")(f slug: string, ) { const loaded = yield* loadCliConfig(cliSettings.workdir, { - goViperCompat: true, search: shouldSearchAncestors(cliSettings), }).pipe(Effect.orElseSucceed(() => null)); yield* Effect.annotateCurrentSpan("config.found", loaded !== null); diff --git a/apps/cli/src/commands/functions/serve/serve.handler.ts b/apps/cli/src/commands/functions/serve/serve.handler.ts index 96838d0d6b..0ca9715e98 100644 --- a/apps/cli/src/commands/functions/serve/serve.handler.ts +++ b/apps/cli/src/commands/functions/serve/serve.handler.ts @@ -1,6 +1,6 @@ import { Effect, Path } from "effect"; import { CommandSettings } from "../../../config/command-settings.service.ts"; -import { functionsGoConfigCompat } from "../../../command-internal/functions-go-config.ts"; +import { functionsLocalConfigLoader } from "../../../command-internal/functions-local-config.ts"; import { DebugFlag, NetworkIdFlag } from "../../../command-internal/global-flags.ts"; import { RuntimeInfo } from "../../../shared/runtime/runtime-info.service.ts"; import { TelemetryState } from "../../../telemetry/telemetry-state.service.ts"; @@ -27,7 +27,6 @@ export const functionsServe = Effect.fn("functions.serve")(function* (flags: Fun debug, networkId, projectIdOverride: cliSettings.projectId, - goViperCompat: true, - goConfigCompat: functionsGoConfigCompat, + localConfigLoader: functionsLocalConfigLoader, }).pipe(Effect.ensuring(telemetryState.flush)); }); diff --git a/apps/cli/src/commands/functions/serve/serve.integration.test.ts b/apps/cli/src/commands/functions/serve/serve.integration.test.ts index 275063814f..3fd6bda899 100644 --- a/apps/cli/src/commands/functions/serve/serve.integration.test.ts +++ b/apps/cli/src/commands/functions/serve/serve.integration.test.ts @@ -40,7 +40,7 @@ import { } from "../../../../tests/helpers/mocks.ts"; import { CommandSettings } from "../../../config/command-settings.service.ts"; import { StackApi } from "../../../command-internal/stack-api.ts"; -import { functionsGoConfigCompat } from "../../../command-internal/functions-go-config.ts"; +import { functionsLocalConfigLoader } from "../../../command-internal/functions-local-config.ts"; import { SUGGEST_CONTAINER_MEMORY_LIMIT } from "../../../command-internal/docker-suggest.ts"; import { DebugFlag, NetworkIdFlag } from "../../../command-internal/global-flags.ts"; import { FileWatcher, type FileWatchEvent } from "../../../shared/runtime/file-watcher.service.ts"; @@ -494,8 +494,7 @@ function serveWithTimers(flags: FunctionsServeFlags, timers: FunctionsServeTimer debug, networkId, projectIdOverride: cliSettings.projectId, - goViperCompat: true, - goConfigCompat: functionsGoConfigCompat, + localConfigLoader: functionsLocalConfigLoader, timers, }).pipe(Effect.ensuring(telemetryState.flush)); }); diff --git a/apps/cli/src/commands/gen/gen.signing-keys-config.ts b/apps/cli/src/commands/gen/gen.signing-keys-config.ts index f5d377dbde..76c3e38998 100644 --- a/apps/cli/src/commands/gen/gen.signing-keys-config.ts +++ b/apps/cli/src/commands/gen/gen.signing-keys-config.ts @@ -1,5 +1,5 @@ import { loadCliProjectEnvironment } from "@supabase/config/effect"; -import { loadCliConfig } from "@supabase/config/internal"; +import { loadCliConfig } from "../../command-internal/cli-config-load.ts"; import { Config, Effect, FileSystem, Option, Path } from "effect"; import { assertDecodableJwkAlgorithm } from "../../command-internal/go-jwt.ts"; import { goJsonKindName } from "../../command-internal/go-json.ts"; @@ -346,7 +346,6 @@ export const resolveSigningKeysConfigPaths = Effect.fnUntraced(function* ( }); const loaded = yield* loadCliConfig(cwd, { cliProjectEnv: projectEnv !== null ? { ...projectEnv, values: projectEnvValues } : undefined, - goViperCompat: true, // `cwd` is already resolved (`CommandSettings.workdir`); `search: false` avoids climbing // again, which would otherwise find an ancestor project's config when `--workdir` points // below another project's root. `tomlOnly: true` because there is no JSON config format. diff --git a/apps/cli/src/commands/gen/types/types.handler.ts b/apps/cli/src/commands/gen/types/types.handler.ts index 53feefe65f..dd1bdd6016 100644 --- a/apps/cli/src/commands/gen/types/types.handler.ts +++ b/apps/cli/src/commands/gen/types/types.handler.ts @@ -1,5 +1,5 @@ import type { LoadedCliConfig } from "@supabase/config/effect"; -import { loadCliConfig } from "@supabase/config/internal"; +import { loadCliConfig } from "../../../command-internal/cli-config-load.ts"; import { ChildProcessSpawner } from "effect/unstable/process"; import { Effect, FileSystem, Option, Path, Stdio, Stream } from "effect"; import { getDomain } from "tldts"; @@ -256,7 +256,6 @@ export const genTypes = Effect.fn("gen.types")(function* (flags: GenTypesFlags) const loadConfig = (projectRef?: string) => loadCliConfig(cliSettings.workdir, { ...(projectRef === undefined ? {} : { projectRef }), - goViperCompat: true, search: shouldSearchAncestors(cliSettings), }).pipe( // `cause.path` names the actual failed file; `loadCliConfig` probes `config.json` diff --git a/apps/cli/src/commands/pull/pull.steps.ts b/apps/cli/src/commands/pull/pull.steps.ts index 3f9564239a..c9ad374423 100644 --- a/apps/cli/src/commands/pull/pull.steps.ts +++ b/apps/cli/src/commands/pull/pull.steps.ts @@ -3,7 +3,7 @@ import { Effect, Option, Path, Stdio } from "effect"; import { CommandPlatformApi } from "../../auth/command-platform-api.service.ts"; import { CommandSettings } from "../../config/command-settings.service.ts"; import { aqua, bold, yellow } from "../../command-internal/colors.ts"; -import { functionsGoConfigCompat } from "../../command-internal/functions-go-config.ts"; +import { functionsLocalConfigLoader } from "../../command-internal/functions-local-config.ts"; import { downloadFunctions, type DownloadFunctionsResult, @@ -144,7 +144,7 @@ export const pullFunctionsStep = Effect.fnUntraced(function* (context: PullStepC api, projectRoot: cliSettings.workdir, rawArgs, - goConfigCompat: functionsGoConfigCompat, + localConfigLoader: functionsLocalConfigLoader, edgeRuntimeVersion, styleEmphasis: (text) => bold(text), styleAqua: (text) => aqua(text), diff --git a/apps/cli/src/commands/secrets/set/set.handler.ts b/apps/cli/src/commands/secrets/set/set.handler.ts index 879bd030b9..818ecc6eaa 100644 --- a/apps/cli/src/commands/secrets/set/set.handler.ts +++ b/apps/cli/src/commands/secrets/set/set.handler.ts @@ -4,7 +4,10 @@ import { type CliConfig, type CliConfigParseError, } from "@supabase/config/effect"; -import { loadCliConfig, resolveCliConfigSubtree } from "@supabase/config/internal"; +import { + loadCliConfig, + resolveCliConfigSubtree, +} from "../../../command-internal/cli-config-load.ts"; import { V1BulkCreateSecretsInput } from "@supabase/api/effect"; import { parse as parseDotenv } from "dotenv"; import { Effect, FileSystem, Option, Path, Redacted, Schema } from "effect"; @@ -115,11 +118,10 @@ export const secretsSet = Effect.fn("secrets.set")(function* (flags: SecretsSetF // // Passing `ref` merges a matching `[remotes.*]` block over the base config before decode, // so a schema-decode error on a remote target recovers that remote's override, not the base - // document. `goViperCompat: true` enables the duplicate-project_id/format checks needed for - // the `DuplicateRemoteProjectIdError` catch below to ever fire. + // document. The CLI loader's duplicate-project_id/format checks are what make the + // `DuplicateRemoteProjectIdError` catch below fire. const loadedConfig = yield* loadCliConfig(runtimeInfo.cwd, { projectRef: ref, - goViperCompat: true, }).pipe( Effect.flatMap((loaded) => { if (loaded === null) { @@ -184,7 +186,6 @@ export const secretsSet = Effect.fn("secrets.set")(function* (flags: SecretsSetF loadedConfig.edge_runtime, projectEnv, "edge_runtime", - { goViperCompat: true }, ); for (const [name, value] of Object.entries(resolved.secrets ?? {})) { // An empty `[edge_runtime.secrets]` value is skipped rather than sent as an diff --git a/apps/cli/src/commands/seed/buckets/buckets.integration.test.ts b/apps/cli/src/commands/seed/buckets/buckets.integration.test.ts index d1dd7650f2..68f190b0e1 100644 --- a/apps/cli/src/commands/seed/buckets/buckets.integration.test.ts +++ b/apps/cli/src/commands/seed/buckets/buckets.integration.test.ts @@ -1,6 +1,6 @@ import { BunServices } from "@effect/platform-bun"; import { describe, expect, it } from "@effect/vitest"; -import { loadCliConfig } from "@supabase/config/internal"; +import { loadCliConfig } from "../../../command-internal/cli-config-load.ts"; import { Cause, ConfigProvider, Effect, Exit, FileSystem, Layer, Option, Path } from "effect"; import * as HttpClient from "effect/unstable/http/HttpClient"; import type * as HttpClientError from "effect/unstable/http/HttpClientError"; @@ -2818,7 +2818,6 @@ describe("seed buckets", () => { ], }); const loaded = yield* loadCliConfig(tmp.current, { - goViperCompat: true, search: false, }).pipe(Effect.provide(BunServices.layer)); if (loaded === null) { diff --git a/apps/cli/src/commands/start/start.handler.ts b/apps/cli/src/commands/start/start.handler.ts index 20d6842ab5..5efe0dd914 100644 --- a/apps/cli/src/commands/start/start.handler.ts +++ b/apps/cli/src/commands/start/start.handler.ts @@ -4,7 +4,7 @@ */ import { BunPath } from "@effect/platform-bun"; import { inferFunctionsManifest } from "@supabase/config/effect"; -import { resolveCliConfigSubtree } from "@supabase/config/internal"; +import { resolveCliConfigSubtree } from "../../command-internal/cli-config-load.ts"; import { Effect, FileSystem, Option, Path, Result } from "effect"; import { FetchHttpClient } from "effect/unstable/http"; import { ChildProcessSpawner } from "effect/unstable/process"; @@ -806,7 +806,6 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { config.functions, { values: projectEnvValues ?? {} }, "functions", - { goViperCompat: true }, ); const configDeclaredFunctions = toPlainFunctionRecord(resolvedFunctions); const configFunctions = yield* inferFunctionsManifest({ @@ -1542,7 +1541,6 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { config.edge_runtime, { values: projectEnvValues ?? {} }, "edge_runtime", - { goViperCompat: true }, ); // Every `config.Secret`-typed field must be decrypted unconditionally so the Edge // Runtime container receives plaintext. `toPlainEdgeRuntimeConfig` only interpolates diff --git a/apps/cli/src/commands/status/status.integration.test.ts b/apps/cli/src/commands/status/status.integration.test.ts index c6fb090829..cfe1c8c75b 100644 --- a/apps/cli/src/commands/status/status.integration.test.ts +++ b/apps/cli/src/commands/status/status.integration.test.ts @@ -619,7 +619,7 @@ project_id = "short" it.live("warns on stderr for a deprecated auth.external provider", () => { // `normalizeDeprecatedExternalProviders` (packages/config/src/io.ts) emits this warning via - // `Console.error` only when `goViperCompat` is set. + // `Console.error` only under the CLI's config-loading semantics. const { layer } = setup(); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); return Effect.gen(function* () { diff --git a/apps/cli/src/commands/stop/stop.integration.test.ts b/apps/cli/src/commands/stop/stop.integration.test.ts index 1e63e8cec6..ac1e80c193 100644 --- a/apps/cli/src/commands/stop/stop.integration.test.ts +++ b/apps/cli/src/commands/stop/stop.integration.test.ts @@ -817,7 +817,7 @@ additional_redirect_urls = "http://a,http://b" it.live("warns on stderr for a deprecated auth.external provider", () => Effect.gen(function* () { // `normalizeDeprecatedExternalProviders` (packages/config/src/io.ts) emits this warning via - // `Console.error` only when `goViperCompat` is set. + // `Console.error` only under the CLI's config-loading semantics. yield* writeSupabaseFile( tempRoot.current, "config.toml", diff --git a/apps/cli/src/commands/storage/storage.frame.ts b/apps/cli/src/commands/storage/storage.frame.ts index ffb0db64c3..1192493f39 100644 --- a/apps/cli/src/commands/storage/storage.frame.ts +++ b/apps/cli/src/commands/storage/storage.frame.ts @@ -1,5 +1,8 @@ import { CliConfigSchema, type CliConfig } from "@supabase/config/effect"; -import { loadCliConfig, type InternalLoadCliConfigOptions } from "@supabase/config/internal"; +import { + loadCliConfig, + type CliConfigLoadOptions, +} from "../../command-internal/cli-config-load.ts"; import { Effect, FileSystem, Schema } from "effect"; import { FetchHttpClient } from "effect/unstable/http"; @@ -50,10 +53,10 @@ export const loadStorageConfig = Effect.fn("Storage.loadConfig")(function* ( cliSettings: { readonly workdir: string; readonly explicitWorkdir: boolean }, projectRef: string, ) { - const loadOptions: InternalLoadCliConfigOptions = + const loadOptions: CliConfigLoadOptions = projectRef !== "" - ? { projectRef, goViperCompat: true, search: shouldSearchAncestors(cliSettings) } - : { goViperCompat: true, search: shouldSearchAncestors(cliSettings) }; + ? { projectRef, search: shouldSearchAncestors(cliSettings) } + : { search: shouldSearchAncestors(cliSettings) }; const loaded = yield* loadCliConfig(cliSettings.workdir, loadOptions).pipe( Effect.catchTag( "CliConfigParseError", diff --git a/apps/cli/src/shared/cli/code-structure.unit.test.ts b/apps/cli/src/shared/cli/code-structure.unit.test.ts index d586ab2f6a..e3b87d02a4 100644 --- a/apps/cli/src/shared/cli/code-structure.unit.test.ts +++ b/apps/cli/src/shared/cli/code-structure.unit.test.ts @@ -3,6 +3,8 @@ import { expect, layer } from "@effect/vitest"; import { Effect, FileSystem, Path, PlatformError } from "effect"; import { fileURLToPath } from "node:url"; +const cliConfigLoadFile = "command-internal/cli-config-load.ts"; + const srcDir = fileURLToPath(new URL("../..", import.meta.url)); const layout = Effect.gen(function* () { @@ -139,4 +141,35 @@ layer(BunServices.layer)("code structure", (it) => { expect(violations).toEqual([]); }), ); + + it.effect("routes CLI config loading through the single cli-config-load wrapper", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const { path } = yield* layout; + const guardedNames = + /\b(?:loadCliConfig|resolveCliConfigValue|resolveCliConfigSubtree|decodeCliConfigDocumentForValidationEffect)\b/; + const internalImport = + /import\s+(?:type\s+)?\{([^}]*)\}\s*from\s*["']@supabase\/config\/internal["']/g; + const compatOption = ["cli", "Compat"].join(""); + const violations: Array = []; + + for (const filePath of (yield* walk(srcDir)).filter((file) => file.endsWith(".ts"))) { + const relativeFile = path.relative(srcDir, filePath).split(path.sep).join("/"); + if (relativeFile === cliConfigLoadFile) continue; + const source = yield* fs.readFileString(filePath); + if (source.includes(compatOption)) { + violations.push(`${relativeFile} mentions ${compatOption}`); + } + for (const match of source.matchAll(internalImport)) { + if (guardedNames.test(match[1]!)) { + violations.push( + `${relativeFile} imports a config loader from @supabase/config/internal`, + ); + } + } + } + + expect(violations).toEqual([]); + }), + ); }); diff --git a/apps/cli/src/shared/functions/deploy.ts b/apps/cli/src/shared/functions/deploy.ts index 0961e0d1a0..2898de467c 100644 --- a/apps/cli/src/shared/functions/deploy.ts +++ b/apps/cli/src/shared/functions/deploy.ts @@ -68,7 +68,7 @@ import { toDockerPath, toSlash, } from "./functions-docker.ts"; -import { loadFunctionsCliConfig, type FunctionsGoConfigCompat } from "./functions-config.ts"; +import { loadFunctionsCliConfig, type FunctionsLocalConfigLoader } from "./functions-config.ts"; import { FunctionsApiStatusError, FunctionsApiTransportError } from "./functions-api.errors.ts"; const COMPRESSED_ESZIP_MAGIC = "EZBR"; @@ -104,10 +104,10 @@ interface DeployFunctionsDependencies { readonly dashboardUrl: string; /** * `undefined` for library callers; the CLI injects - * `functionsGoConfigCompat` so this file never imports the command tree - * directly — see {@link FunctionsGoConfigCompat}. + * `functionsLocalConfigLoader` so this file never imports the command tree + * directly — see {@link FunctionsLocalConfigLoader}. */ - readonly goConfigCompat: FunctionsGoConfigCompat | undefined; + readonly localConfigLoader: FunctionsLocalConfigLoader | undefined; readonly yes?: boolean; readonly rawArgs: ReadonlyArray; readonly edgeRuntimeVersion: string; @@ -2505,7 +2505,7 @@ export const deployFunctions = Effect.fn("functions.deploy")(function* < const context = yield* loadFunctionsCliConfig({ projectRoot: dependencies.projectRoot, projectRef, - goConfigCompat: dependencies.goConfigCompat, + localConfigLoader: dependencies.localConfigLoader, }); if (flags.functionNames.length > 0) { @@ -2534,7 +2534,7 @@ export const deployFunctions = Effect.fn("functions.deploy")(function* < // Matches `loadFunctionsCliConfig`'s own options above: no ancestor directory is searched // past `dependencies.projectRoot` for either load, so they can never resolve two // different projects. - search: dependencies.goConfigCompat === undefined, + search: dependencies.localConfigLoader === undefined, }); const configDeclaredFunctions = deployConfig?.functions ?? {}; const rawConfigFunctions = rawFunctionConfigRecord(context.loaded?.document); diff --git a/apps/cli/src/shared/functions/download.ts b/apps/cli/src/shared/functions/download.ts index f0f5d8bc9b..5f2d69961b 100644 --- a/apps/cli/src/shared/functions/download.ts +++ b/apps/cli/src/shared/functions/download.ts @@ -28,7 +28,7 @@ import { resolveFunctionsDockerImage, runChildProcess, } from "./functions-docker.ts"; -import { loadFunctionsCliConfig, type FunctionsGoConfigCompat } from "./functions-config.ts"; +import { loadFunctionsCliConfig, type FunctionsLocalConfigLoader } from "./functions-config.ts"; import { edgeRuntimeImage, FUNCTIONS_DOWNLOAD_BUNDLER_MUTEX_GROUP, @@ -99,9 +99,9 @@ interface EdgeRuntimeImageDependencies { readonly projectRoot: string; /** * `undefined` for library callers; the CLI injects this so this file - * never imports the command tree directly — see {@link FunctionsGoConfigCompat}. + * never imports the command tree directly — see {@link FunctionsLocalConfigLoader}. */ - readonly goConfigCompat: FunctionsGoConfigCompat | undefined; + readonly localConfigLoader: FunctionsLocalConfigLoader | undefined; /** * Fallback edge-runtime image tag used when the project config doesn't * pin `edge_runtime.deno_version` to `1`. Mirrors `deploy.ts`'s own @@ -873,7 +873,7 @@ const resolveEdgeRuntimeImage = Effect.fn("functions.download.resolveEdgeRuntime const context = yield* loadFunctionsCliConfig({ projectRoot: dependencies.projectRoot, projectRef, - goConfigCompat: dependencies.goConfigCompat, + localConfigLoader: dependencies.localConfigLoader, }); const edgeRuntimeVersion = yield* resolveEdgeRuntimeVersion( context.denoVersion, diff --git a/apps/cli/src/shared/functions/functions-config.ts b/apps/cli/src/shared/functions/functions-config.ts index 77a20c5d41..c41e67b021 100644 --- a/apps/cli/src/shared/functions/functions-config.ts +++ b/apps/cli/src/shared/functions/functions-config.ts @@ -7,7 +7,7 @@ import { normalizeProjectId } from "./functions-docker.ts"; /** * Config resolution context shared by the `functions` Docker paths * (`deploy`, `download`, `serve`). Callers that inject - * {@link FunctionsGoConfigCompat} additionally run the config/dotenv + * {@link FunctionsLocalConfigLoader} additionally run the config/dotenv * validation pipeline `start`/`stop`/`status` already share; callers that * omit it keep the plain `loadCliConfig` behavior. */ @@ -25,7 +25,7 @@ interface FunctionsCliConfigContext { * without this shared module importing the command tree's validation * machinery directly. `undefined` disables the hook. */ -export interface FunctionsGoConfigCompat { +export interface FunctionsLocalConfigLoader { readonly load: (input: { readonly projectRoot: string; readonly projectRef: string | undefined; @@ -43,18 +43,18 @@ export interface FunctionsGoConfigCompat { /** * Loads project config for a `functions` command. Callers that provide - * `goConfigCompat` run its dotenv/config-validate pipeline before any + * `localConfigLoader` run its dotenv/config-validate pipeline before any * Docker/API work; callers that don't fall back to `loadCliConfig`. */ export const loadFunctionsCliConfig = Effect.fn("FunctionsConfig.load")(function* (input: { readonly projectRoot: string; readonly projectRef: string | undefined; - readonly goConfigCompat: FunctionsGoConfigCompat | undefined; + readonly localConfigLoader: FunctionsLocalConfigLoader | undefined; }) { yield* Effect.annotateCurrentSpan({ - "config.go_compat": input.goConfigCompat !== undefined, + "config.local_validation": input.localConfigLoader !== undefined, }); - if (input.goConfigCompat === undefined) { + if (input.localConfigLoader === undefined) { const path = yield* Path.Path; const loaded = yield* loadCliConfig( input.projectRoot, @@ -73,7 +73,7 @@ export const loadFunctionsCliConfig = Effect.fn("FunctionsConfig.load")(function } satisfies FunctionsCliConfigContext; } - const context = yield* input.goConfigCompat.load({ + const context = yield* input.localConfigLoader.load({ projectRoot: input.projectRoot, projectRef: input.projectRef, }); diff --git a/apps/cli/src/shared/functions/serve.ts b/apps/cli/src/shared/functions/serve.ts index fe1c997214..146ea74236 100644 --- a/apps/cli/src/shared/functions/serve.ts +++ b/apps/cli/src/shared/functions/serve.ts @@ -8,11 +8,6 @@ import { type ResolvedCliConfigValue, type ResolvedFunctionConfig as ManifestFunctionConfig, } from "@supabase/config/effect"; -import { - loadCliConfig, - resolveCliConfigSubtree, - resolveCliConfigValue, -} from "@supabase/config/internal"; import { defaultJwtSecret, defaultPublishableKey, @@ -48,6 +43,11 @@ import { isContainerNotFoundMessage, spawnContainerCli, } from "../../command-internal/container-cli.ts"; +import { + loadCliConfig, + resolveCliConfigSubtree, + resolveCliConfigValue, +} from "../../command-internal/cli-config-load.ts"; import { inspectContainerState } from "../../command-internal/docker-lifecycle.ts"; import { isDockerDaemonUnreachable } from "../../command-internal/docker-suggest.ts"; import { parseDotEnv } from "../../command-internal/dotenv.ts"; @@ -88,7 +88,7 @@ import { runChildProcess, toDockerPath, } from "./functions-docker.ts"; -import { loadFunctionsCliConfig, type FunctionsGoConfigCompat } from "./functions-config.ts"; +import { loadFunctionsCliConfig, type FunctionsLocalConfigLoader } from "./functions-config.ts"; import { edgeRuntimeImage, resolveEdgeRuntimeVersionPin } from "./functions.shared.ts"; import { slimImagesEnabled } from "../services/slim-images.ts"; import { @@ -183,13 +183,11 @@ export interface FunctionsServeDependencies { readonly debug: boolean; readonly networkId: Option.Option; readonly projectIdOverride: Option.Option; - readonly goViperCompat: boolean; /** * `undefined` for library callers; the CLI injects this so this file - * never imports the command tree directly — see {@link FunctionsGoConfigCompat}. - * Distinct from `goViperCompat` above, which only gates `env(...)` interpolation. + * never imports the command tree directly — see {@link FunctionsLocalConfigLoader}. */ - readonly goConfigCompat: FunctionsGoConfigCompat | undefined; + readonly localConfigLoader: FunctionsLocalConfigLoader | undefined; /** Overrides the shutdown-grace and log-retry timers; production leaves this unset. */ readonly timers?: FunctionsServeTimers; } @@ -709,14 +707,13 @@ const finalizeAuthArtifacts = Effect.fn("functions.serve.finalizeAuthArtifacts") const resolveServeConfig = Effect.fn("functions.serve.resolveConfig")(function* ( projectRoot: string, projectIdOverride: Option.Option, - goViperCompat: boolean, - goConfigCompat: FunctionsGoConfigCompat | undefined, + localConfigLoader: FunctionsLocalConfigLoader | undefined, ) { const path = yield* Path.Path; // Keeps `.env` discovery, config load, and functions-manifest inference // from resolving three different roots: the CLI's `search: false` must // match `loadFunctionsCliConfig`'s own options exactly (see below). - const searchAncestors = goConfigCompat === undefined; + const searchAncestors = localConfigLoader === undefined; const projectEnv = yield* loadServeCliProjectEnvironment(projectRoot, { search: searchAncestors, }); @@ -735,41 +732,34 @@ const resolveServeConfig = Effect.fn("functions.serve.resolveConfig")(function* // `search`/`tomlOnly` here must match `loadFunctionsCliConfig`'s own // options below exactly, or the two loads can resolve two different files, // silently mixing fields from two different projects. Library callers - // (`goConfigCompat === undefined`) keep the package defaults unchanged. + // (`localConfigLoader === undefined`) keep the package defaults unchanged. const loadedConfig = yield* loadCliConfig(projectRoot, { ...(projectRef === undefined ? {} : { projectRef }), ...(projectEnv === null ? {} : { cliProjectEnv: projectEnv }), - goViperCompat, search: searchAncestors, - ...(goConfigCompat === undefined ? {} : { tomlOnly: true }), + ...(localConfigLoader === undefined ? {} : { tomlOnly: true }), }); const baseConfig = loadedConfig?.config ?? defaultCliConfig; const auth = projectEnv === null ? toPlainAuthConfig(baseConfig.auth) - : toPlainAuthConfig( - yield* resolveCliConfigSubtree(baseConfig.auth, projectEnv, "auth", { goViperCompat }), - ); + : toPlainAuthConfig(yield* resolveCliConfigSubtree(baseConfig.auth, projectEnv, "auth")); const edgeRuntime = projectEnv === null ? toPlainEdgeRuntimeConfig(baseConfig.edge_runtime) : toPlainEdgeRuntimeConfig( - yield* resolveCliConfigSubtree(baseConfig.edge_runtime, projectEnv, "edge_runtime", { - goViperCompat, - }), + yield* resolveCliConfigSubtree(baseConfig.edge_runtime, projectEnv, "edge_runtime"), ); const apiPort = projectEnv === null ? baseConfig.api.port - : (yield* resolveCliConfigSubtree(baseConfig.api, projectEnv, "api", { goViperCompat })).port; + : (yield* resolveCliConfigSubtree(baseConfig.api, projectEnv, "api")).port; const configDeclaredFunctions = projectEnv === null ? toPlainFunctionRecord(baseConfig.functions) : toPlainFunctionRecord( - yield* resolveCliConfigSubtree(baseConfig.functions, projectEnv, "functions", { - goViperCompat, - }), + yield* resolveCliConfigSubtree(baseConfig.functions, projectEnv, "functions"), ); const configForManifest: CliConfig = { ...baseConfig, @@ -784,9 +774,7 @@ const resolveServeConfig = Effect.fn("functions.serve.resolveConfig")(function* projectEnv === null ? (baseConfig.project_id ?? "") : (reveal( - yield* resolveCliConfigValue(baseConfig.project_id ?? "", projectEnv, "project_id", { - goViperCompat, - }), + yield* resolveCliConfigValue(baseConfig.project_id ?? "", projectEnv, "project_id"), ) ?? ""); const rawProjectId = Option.getOrElse(projectIdOverride, () => configProjectId).trim(); const fallbackProjectId = path.basename(path.resolve(projectRoot)); @@ -798,12 +786,12 @@ const resolveServeConfig = Effect.fn("functions.serve.resolveConfig")(function* // project dotenv, so a project setting it only in `.env` gets a different // Docker network than `deploy`/`download`/`start` — a silently broken `serve`. const goContext = - goConfigCompat === undefined + localConfigLoader === undefined ? undefined : yield* loadFunctionsCliConfig({ projectRoot, projectRef, - goConfigCompat, + localConfigLoader, }); return { @@ -2006,8 +1994,7 @@ const startEdgeRuntime = Effect.fn("functions.serve.startEdgeRuntime")(function* const resolved = yield* resolveServeConfig( input.dependencies.projectRoot, input.dependencies.projectIdOverride, - input.dependencies.goViperCompat, - input.dependencies.goConfigCompat, + input.dependencies.localConfigLoader, ); const projectId = resolved.projectId; const containerId = localDockerId("edge_runtime", projectId); diff --git a/docs/adr/0023-config-pull-write-strategy-and-scope-resolution.md b/docs/adr/0023-config-pull-write-strategy-and-scope-resolution.md index bb8359393a..5d3af9c481 100644 --- a/docs/adr/0023-config-pull-write-strategy-and-scope-resolution.md +++ b/docs/adr/0023-config-pull-write-strategy-and-scope-resolution.md @@ -45,7 +45,7 @@ A live dogfooding run surfaced a design gap the acceptance criteria above didn't **Layer 1 — plan fixpoint expansion.** After the base plan, `pull.plan.ts`'s `legacyExpandConfigPullChangeSet` projects the planned writes onto the SAME `{config, document}` pair the classifier diffed from and re-runs `diffProjectConfig` against the same remote — the exact "projected next document" machinery the planner-defect check (§ Consequences, below) already used, just iterated instead of run once. A newly un-gated sibling (a path ADR 0021's gate excluded before the write, but not after) is absorbed into the SAME plan, through the SAME skip rules as any other change (an `env()`-sourced sibling is still never overwritten; a `local_only` sibling the remote never reported is still never written) and the SAME warning derivation (a newly-absorbed `dual_scope`/`duplicates_root`/`array_drift` candidate is flagged exactly like a first-round one — the fixpoint's output is classified in ONE pass over the union of every round's changes, not accumulated plan-by-plan). Repeats until a round finds nothing new to project, capped at 4 rounds — die-free: hitting the cap simply stops absorbing further rounds, leaving layer 2 as the actual safety net. -**Layer 2 — schema-validation gate.** Before the pre-write TOCTOU re-read, the projected FINAL document — every surviving planned write, plus a newly-created block's `project_id`, applied to the raw on-disk document shape — is decoded through `@supabase/config`'s real `CliConfigSchema` decode (`decodeCliConfigDocumentForValidationEffect`, `@supabase/config/internal`), with the same `goViperCompat` semantics the next `loadCliConfig` call will apply and its own environment resolution (see below). This composes the SAME env-interpolation → schema-decode steps `loadCliConfigFile` itself runs, minus the raw parse that doesn't apply to an in-memory document. On a decode failure, every planned write under the nearest enclosing family/provider table of each failing path is dropped (walking upward from the failing path for the deepest ancestor declaring an `enabled` key, falling back to the immediate parent) — reported as a NEW skip reason, `would_invalidate`, plus a `would_invalidate` warning naming the still-missing field(s) and, when a missing field's local spelling is itself an unresolved `env(VAR)` reference, the exact variable to set. The document is re-projected and re-validated, capped at 4 rounds; if it still fails once nothing more can be dropped, the whole command fails with a typed, never-user-actionable error (`LegacyConfigPullValidationFailedError`) rather than writing — dropping a family always restores a subtree to its pre-pull state, which loaded successfully at the start of the very same command, so exhausting the cap without success means the drop heuristic missed the right family or something else is structurally broken, not a real user-facing condition. +**Layer 2 — schema-validation gate.** Before the pre-write TOCTOU re-read, the projected FINAL document — every surviving planned write, plus a newly-created block's `project_id`, applied to the raw on-disk document shape — is decoded through `@supabase/config`'s real `CliConfigSchema` decode (`decodeCliConfigDocumentForValidationEffect`, `@supabase/config/internal`), with the same `cliCompat` semantics the next `loadCliConfig` call will apply and its own environment resolution (see below). This composes the SAME env-interpolation → schema-decode steps `loadCliConfigFile` itself runs, minus the raw parse that doesn't apply to an in-memory document. On a decode failure, every planned write under the nearest enclosing family/provider table of each failing path is dropped (walking upward from the failing path for the deepest ancestor declaring an `enabled` key, falling back to the immediate parent) — reported as a NEW skip reason, `would_invalidate`, plus a `would_invalidate` warning naming the still-missing field(s) and, when a missing field's local spelling is itself an unresolved `env(VAR)` reference, the exact variable to set. The document is re-projected and re-validated, capped at 4 rounds; if it still fails once nothing more can be dropped, the whole command fails with a typed, never-user-actionable error (`LegacyConfigPullValidationFailedError`) rather than writing — dropping a family always restores a subtree to its pre-pull state, which loaded successfully at the start of the very same command, so exhausting the cap without success means the drop heuristic missed the right family or something else is structurally broken, not a real user-facing condition. Layer 2's validation mirrors the loader's OWN environment resolution exactly — the current process environment layered with the project's `.env`/`.env.local` (`decodeCliConfigDocumentForValidationEffect`, `@supabase/config/internal`), not bare `process.env` — and, for a `[remotes.*]` destination, ALSO decodes the write projected onto the overlay-merged document (the same merge a future `loadCliConfig` targeting that project ref applies before its own checks-enabled decode), since a block's business-rule checks are disabled until it is actually selected and a write can therefore pass unmerged yet still fail once merged. A decode failure whose path ALREADY failed in the raw, pre-write document — checked the same way, before any of this run's writes are projected — is pre-existing: never attributed to this plan, never dropped, never a reason to fail the command, since the file was already in that state and pulling only ever needs to leave it no worse. diff --git a/packages/config/AGENTS.md b/packages/config/AGENTS.md index 7d9f384033..0f36ee5637 100644 --- a/packages/config/AGENTS.md +++ b/packages/config/AGENTS.md @@ -30,10 +30,10 @@ artifacts (`./schema.json`, `./project-schema.json`). project-environment resolution, and `inferFunctionsManifest` (discovers and validates `supabase/functions/*` on disk). - `@supabase/config/internal` (CLI-2234) — NOT covered by semver, and only `apps/cli` may import it - (enforced by `src/monorepo-import-contract.unit.test.ts`). Carries `apps/cli`'s own Go-parity - call sites and contract-guard tests: `loadCliConfig`/`resolveCliConfigValue`/ + (enforced by `src/monorepo-import-contract.unit.test.ts`). Carries `apps/cli`'s own + config-loading entrypoints and contract-guard tests: `loadCliConfig`/`resolveCliConfigValue`/ `resolveCliConfigSubtree` — the SAME runtime functions `./effect` exports, re-typed here to - additionally accept the internal-only `goViperCompat` option (`InternalLoadCliConfigOptions` for + additionally accept the internal-only `cliCompat` option (`InternalLoadCliConfigOptions` for `loadCliConfig`; `resolveCliConfigValue`/`resolveCliConfigSubtree`'s own widened options type, `InternalResolveCliConfigOptions`, is package-internal and not itself re-exported) — plus the otherwise-internal registry data (`AUTH_HOOK_NAMES`, `unmappedSecretApiPaths`, @@ -65,8 +65,8 @@ artifacts (`./schema.json`, `./project-schema.json`). from `@supabase/config`. - `@supabase/config/io` is exclusively for external consumers outside this monorepo that aren't Effect-native. Do not add an internal consumer of it. -- `@supabase/config/internal` is for `apps/cli`'s own Go-parity call sites and contract-guard - tests only — a symbol that needs the internal-only `goViperCompat` typings, or the internal +- `@supabase/config/internal` is for `apps/cli`'s own config-loading entrypoints and contract-guard + tests only — a symbol that needs the internal-only `cliCompat` typings, or the internal registry data, imports it from there; every other symbol in the same import statement stays on its public specifier (`.`/`./effect`). Enforced: every `@supabase/config/internal` occurrence outside this package must be under `apps/cli/`. diff --git a/packages/config/README.md b/packages/config/README.md index b118d12e3e..b46b987a14 100644 --- a/packages/config/README.md +++ b/packages/config/README.md @@ -73,14 +73,14 @@ for the full vocabulary. ## Entrypoints -| Entrypoint | Contents | Constraints | -| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `.` | `CliConfigSchema`/`ProjectConfigSchema` and their types, config encoding, sparse-config defaults, the `ProjectConfig` converters, the config-diff classification engine (`diffProjectConfig`), error classes | Pure — browser/edge/Node/Bun-safe. No file IO, no Effect-returning function, no `@effect/platform-*`/`node:`/`bun:` module anywhere in its transitive import graph | -| `./io` | A Promise-based facade over the same file-IO/Effect programs as `./effect` | Resolved automatically via package.json export conditions (`bun`/`node`/`browser`/`default`). Requires one of the optional platform peers — `@effect/platform-bun` under Bun, `@effect/platform-node` under Node — installed at runtime; see "Installing" below for the failure mode when it's missing | -| `./effect` | Effect-native superset of `.`: `CliConfigStore`/`cliConfigStoreLayer`, config loading/saving, project-environment resolution, functions-manifest inference | Requires `effect`; requires a platform peer only for the file-IO programs, exactly like `./io` | -| `./internal` | `ENV_CAPTURE_REGEX`, `AUTH_HOOK_NAMES`, `unmappedSecretApiPaths`, `projectConfigMappingRows`, the `ProjectConfigMappingRow`/`ProjectConfigApiAttributes`/`InternalLoadCliConfigOptions` types, plus `loadCliConfig`/`resolveCliConfigValue`/`resolveCliConfigSubtree` re-typed to additionally accept the internal-only `goViperCompat` option — the SAME runtime functions `./effect` exports, not independent implementations | **Not covered by semver, and only `apps/cli` may import it** (enforced by `src/monorepo-import-contract.unit.test.ts`). Exists solely for the Supabase CLI's own use and its contract-guard tests — any export here (its existence, shape, or behavior) can change or vanish in any release without notice | -| `./schema.json` | Generated JSON Schema for `CliConfig` | Draft 2020-12 — a language-agnostic contract for non-TypeScript consumers | -| `./project-schema.json` | Generated JSON Schema for `ProjectConfig` | Draft 2020-12 — a language-agnostic contract for non-TypeScript consumers | +| Entrypoint | Contents | Constraints | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `.` | `CliConfigSchema`/`ProjectConfigSchema` and their types, config encoding, sparse-config defaults, the `ProjectConfig` converters, the config-diff classification engine (`diffProjectConfig`), error classes | Pure — browser/edge/Node/Bun-safe. No file IO, no Effect-returning function, no `@effect/platform-*`/`node:`/`bun:` module anywhere in its transitive import graph | +| `./io` | A Promise-based facade over the same file-IO/Effect programs as `./effect` | Resolved automatically via package.json export conditions (`bun`/`node`/`browser`/`default`). Requires one of the optional platform peers — `@effect/platform-bun` under Bun, `@effect/platform-node` under Node — installed at runtime; see "Installing" below for the failure mode when it's missing | +| `./effect` | Effect-native superset of `.`: `CliConfigStore`/`cliConfigStoreLayer`, config loading/saving, project-environment resolution, functions-manifest inference | Requires `effect`; requires a platform peer only for the file-IO programs, exactly like `./io` | +| `./internal` | `ENV_CAPTURE_REGEX`, `AUTH_HOOK_NAMES`, `unmappedSecretApiPaths`, `projectConfigMappingRows`, the `ProjectConfigMappingRow`/`ProjectConfigApiAttributes`/`InternalLoadCliConfigOptions` types, plus `loadCliConfig`/`resolveCliConfigValue`/`resolveCliConfigSubtree` re-typed to additionally accept the internal-only `cliCompat` option — the SAME runtime functions `./effect` exports, not independent implementations | **Not covered by semver, and only `apps/cli` may import it** (enforced by `src/monorepo-import-contract.unit.test.ts`). Exists solely for the Supabase CLI's own use and its contract-guard tests — any export here (its existence, shape, or behavior) can change or vanish in any release without notice | +| `./schema.json` | Generated JSON Schema for `CliConfig` | Draft 2020-12 — a language-agnostic contract for non-TypeScript consumers | +| `./project-schema.json` | Generated JSON Schema for `ProjectConfig` | Draft 2020-12 — a language-agnostic contract for non-TypeScript consumers | A few things worth calling out beyond the table: diff --git a/packages/config/docs/cli-config-loading.md b/packages/config/docs/cli-config-loading.md index 31f55c6aef..8ae71a077b 100644 --- a/packages/config/docs/cli-config-loading.md +++ b/packages/config/docs/cli-config-loading.md @@ -260,13 +260,13 @@ These helpers do two things at once: `resolveCliConfigSubtree` walks recursively through objects, arrays, and records, so it also resolves and redacts leaves nested inside `[remotes.*]` blocks. -An optional `goViperCompat` flag switches the `env(NAME)` matcher from the default, strict -`SCREAMING_SNAKE_CASE`-only pattern to Go/viper's case-agnostic `^env\((.*)\)$` form; only the -Go-parity CLI sets it. The public `resolveCliConfigValue`/`resolveCliConfigSubtree` on -`.`/`./effect` take no options parameter at all (CLI-2234) — `goViperCompat` is internal-only, +An optional `cliCompat` flag switches the `env(NAME)` matcher from the default, strict +`SCREAMING_SNAKE_CASE`-only pattern to the Supabase CLI's case-agnostic `^env\((.*)\)$` form; only +`apps/cli` sets it. The public `resolveCliConfigValue`/`resolveCliConfigSubtree` on +`.`/`./effect` take no options parameter at all (CLI-2234) — `cliCompat` is internal-only, typed on `InternalResolveCliConfigOptions`, a package-internal type that is not itself exported. `@supabase/config/internal` re-exports these same runtime functions re-typed to additionally -accept it; `apps/cli`'s Go-parity call sites import from there instead. +accept it; `apps/cli` imports from there instead. Callers such as `functions serve`/`functions dev`, `secrets set`, and `start` call these resolvers on the subtrees they actually need (e.g. `auth`, `edge_runtime`, `functions`), so dormant diff --git a/packages/config/src/config-document.ts b/packages/config/src/config-document.ts index f7eecffb4e..3ecfa29e1c 100644 --- a/packages/config/src/config-document.ts +++ b/packages/config/src/config-document.ts @@ -84,7 +84,7 @@ export const cliConfigValueSourceAt = ( * When `projectRef` is set, the matching `[remotes.]` block is merged over the base * config before decode; omitting it loads the base config verbatim. Duplicate-`project_id` and * project-ref-format checks across every `[remotes.*]` block only run when - * {@link InternalLoadCliConfigOptions.goViperCompat} is `true`, so callers that never opt into + * {@link InternalLoadCliConfigOptions.cliCompat} is `true`, so callers that never opt into * that mode aren't broken by an unrelated malformed remote block. */ export interface LoadCliConfigOptions { @@ -107,13 +107,13 @@ export interface LoadCliConfigOptions { */ export interface InternalLoadCliConfigOptions extends LoadCliConfigOptions { /** - * Opts into Go/viper-parity decode and validation semantics: duplicate-`project_id` and + * Opts into the Supabase CLI's decode and validation semantics: duplicate-`project_id` and * project-ref-format checks run across every `[remotes.*]` block even without a `projectRef`, * deprecated `auth.external.{linkedin,slack}` blocks warn on stderr, `env(...)` matching is - * case-agnostic, and comma-separated strings coerce into `[]string`-typed fields regardless of - * origin. Defaults to `false`, which `packages/stack` and the functions manifest rely on. + * case-agnostic, and comma-separated strings coerce into array-typed fields regardless of + * origin. Defaults to `false`, the strict behaviour of the public `@supabase/config` API. */ - readonly goViperCompat?: boolean; + readonly cliCompat?: boolean; } export interface SaveCliConfigOptions { diff --git a/packages/config/src/effect.ts b/packages/config/src/effect.ts index 36e0e79c42..705d83a798 100644 --- a/packages/config/src/effect.ts +++ b/packages/config/src/effect.ts @@ -10,7 +10,7 @@ import * as project from "./project.ts"; export { configJsonPath, configTomlPath, saveCliConfig } from "./io.ts"; export { validateCliConfig } from "./validate.ts"; -/** Loads a CLI config document; the internal `goViperCompat` option is exposed only via `@supabase/config/internal`. */ +/** Loads a CLI config document; the internal `cliCompat` option is exposed only via `@supabase/config/internal`. */ export const loadCliConfig: ( cwd: string, options?: LoadCliConfigOptions, @@ -27,7 +27,7 @@ export { loadDotEnvFile, loadCliProjectEnvironment } from "./project.ts"; /** * Effect-typed counterpart to the sync `resolveCliConfigValue` exported from `.`; the internal - * `goViperCompat` option is exposed only via `@supabase/config/internal`. + * `cliCompat` option is exposed only via `@supabase/config/internal`. */ export const resolveCliConfigValue: ( value: T, diff --git a/packages/config/src/internal.ts b/packages/config/src/internal.ts index e7e4c33a36..a38f7cbc1a 100644 --- a/packages/config/src/internal.ts +++ b/packages/config/src/internal.ts @@ -4,7 +4,7 @@ * * `loadCliConfig`/`resolveCliConfigValue`/`resolveCliConfigSubtree` below are the same runtime * functions `./effect` exports, re-typed here to widen their options parameter to the - * internal-only `goViperCompat` knob. + * internal-only `cliCompat` knob. */ export { ENV_CAPTURE_REGEX } from "./lib/env.ts"; export { diff --git a/packages/config/src/io.ts b/packages/config/src/io.ts index 82bffc9e6f..e63ce2ee8d 100644 --- a/packages/config/src/io.ts +++ b/packages/config/src/io.ts @@ -87,7 +87,7 @@ function pathKey(path: ReadonlyArray): string { /** * Builds a `project_id -> "[remotes.]"` map across every `[remotes.*]` block, failing on - * the first duplicate. {@link applyRemoteOverride} only invokes this when `goViperCompat` is + * the first duplicate. {@link applyRemoteOverride} only invokes this when `cliCompat` is * set, so it runs even for callers that don't request a specific `projectRef`. A missing * `project_id` reads as `""`, so two remotes that both omit it collide on the empty key. */ @@ -174,7 +174,7 @@ const applyRemoteOverride = Effect.fnUntraced(function* ( rawDocument: Record, interpolatedRemotes: Record | undefined, projectRef: string | undefined, - goViperCompat: boolean, + cliCompat: boolean, ) { const remotes = rawDocument["remotes"]; if (!isObject(remotes)) { @@ -184,7 +184,7 @@ const applyRemoteOverride = Effect.fnUntraced(function* ( remoteLeafPaths: [], }; } - if (goViperCompat) { + if (cliCompat) { yield* checkDuplicateRemoteProjectIds(remotes); yield* checkRemoteProjectIdFormat(interpolatedRemotes ?? remotes); } @@ -482,7 +482,7 @@ export interface DecodeCliConfigDocumentForValidationEffectOptions { */ readonly path: string; readonly format: ConfigFormat; - readonly goViperCompat?: boolean; + readonly cliCompat?: boolean; /** * When set, merges the `[remotes.]` block over the root before decoding, so it's * checked against the root's full business rules instead of the relaxed treatment an @@ -535,7 +535,7 @@ export const decodeCliConfigDocumentForValidationEffect = Effect.fn( cwd: projectRoot, baseEnv: process.env, }); - const goViperCompat = options.goViperCompat ?? false; + const cliCompat = options.cliCompat ?? false; const { document: documentForDecode, appliedRemote } = mergeSelectedRemoteForValidation( document, @@ -546,7 +546,7 @@ export const decodeCliConfigDocumentForValidationEffect = Effect.fn( documentForDecode, cliProjectEnv?.values ?? {}, CliConfigSchema, - { goViperCompat }, + { cliCompat }, ); const { document: normalizedForDecode } = normalizeDeprecatedExternalProviders(interpolated); return yield* parseCliConfig(normalizedForDecode, options.format, options.path, appliedRemote); @@ -607,13 +607,13 @@ export const loadCliConfigFile = Effect.fn("CliConfig.loadFile")(function* ( baseEnv: process.env, search: options?.search, })); - const goViperCompat = options?.goViperCompat ?? false; + const cliCompat = options?.cliCompat ?? false; const interpolateDocument = ( document: unknown, onResolvedEnv?: (path: ReadonlyArray, envNames: ReadonlyArray) => void, ): unknown => interpolateEnvReferencesAgainstSchema(document, cliProjectEnv?.values ?? {}, CliConfigSchema, { - goViperCompat, + cliCompat, onResolvedEnv, }); @@ -627,7 +627,7 @@ export const loadCliConfigFile = Effect.fn("CliConfig.loadFile")(function* ( // Merge the matching `[remotes.*]` override over the raw, pre-`env()` document (see // `applyRemoteOverride`). The match/merge always runs; the duplicate-`project_id`/format - // checks only run when `goViperCompat` is set. + // checks only run when `cliCompat` is set. let documentForDecode: unknown = normalized; let appliedRemote: string | undefined; let remoteLeafPaths: Array = []; @@ -636,7 +636,7 @@ export const loadCliConfigFile = Effect.fn("CliConfig.loadFile")(function* ( normalized, interpolatedRemotes, options?.projectRef, - goViperCompat, + cliCompat, ); documentForDecode = resolved.document; appliedRemote = resolved.appliedRemote; @@ -664,7 +664,7 @@ export const loadCliConfigFile = Effect.fn("CliConfig.loadFile")(function* ( removedProviders, } = normalizeDeprecatedExternalProviders(documentForDecode); // Pinned to the real console, same as the `[inbucket]` warning above. - if (goViperCompat) { + if (cliCompat) { for (const ext of deprecatedProviders) { yield* Console.error( `WARN: disabling deprecated "${ext}" provider. Please use [auth.external.${ext}_oidc] instead`, diff --git a/packages/config/src/io.unit.test.ts b/packages/config/src/io.unit.test.ts index a6a219c264..b9d6fe61e7 100644 --- a/packages/config/src/io.unit.test.ts +++ b/packages/config/src/io.unit.test.ts @@ -459,8 +459,6 @@ stack = true } }); - // Go-parity callers (legacy `status`/`stop`) pass `tomlOnly: true` so a stray `config.json` - // never wins over `config.toml`. test("loads TOML instead of JSON when tomlOnly is set, even if JSON exists", async () => { const cwd = makeTempProject(); const jsonPath = await runConfigEffect(configJsonPath(cwd)); @@ -1103,7 +1101,7 @@ enabled = "env(SUPABASE_ANALYTICS_ENABLED)" test("splits a comma-separated string literal into a slice (Go's StringToSliceHookFunc)", async () => { // A plain string value for a `[]string` field like `additional_redirect_urls` decodes fine - // when `goViperCompat` is set, not just via `env(...)`. + // when `cliCompat` is set, not just via `env(...)`. const cwd = makeTempProject(); try { @@ -1117,7 +1115,7 @@ additional_redirect_urls = "http://a,http://b" `, ); - const loaded = await runConfigEffect(loadCliConfig(cwd, { goViperCompat: true })); + const loaded = await runConfigEffect(loadCliConfig(cwd, { cliCompat: true })); expect(loaded!.config.auth.additional_redirect_urls).toEqual(["http://a", "http://b"]); } finally { await rm(cwd, { recursive: true, force: true }); @@ -1139,7 +1137,7 @@ additional_redirect_urls = "env(SUPABASE_REDIRECT_URLS)" ); await writeFile(join(cwd, "supabase", ".env"), "SUPABASE_REDIRECT_URLS=http://a,http://b\n"); - const loaded = await runConfigEffect(loadCliConfig(cwd, { goViperCompat: true })); + const loaded = await runConfigEffect(loadCliConfig(cwd, { cliCompat: true })); expect(loaded!.config.auth.additional_redirect_urls).toEqual(["http://a", "http://b"]); } finally { await rm(cwd, { recursive: true, force: true }); @@ -1160,7 +1158,7 @@ additional_redirect_urls = "" `, ); - const loaded = await runConfigEffect(loadCliConfig(cwd, { goViperCompat: true })); + const loaded = await runConfigEffect(loadCliConfig(cwd, { cliCompat: true })); expect(loaded!.config.auth.additional_redirect_urls).toEqual([]); } finally { await rm(cwd, { recursive: true, force: true }); @@ -1290,9 +1288,9 @@ port = "env(SUPABASE_DB_PORT_TEST)" } }); - // Pins pre-Go-parity default behavior, so `packages/stack` and the functions manifest (which - // don't pass `goViperCompat`) don't inherit the Go-parity CLI's stricter/wider semantics. - test("loads successfully with a duplicate [remotes.*] project_id when goViperCompat is omitted", async () => { + // Pins the strict default so callers that don't pass `cliCompat` don't inherit the CLI's + // config-loading semantics. + test("loads successfully with a duplicate [remotes.*] project_id when cliCompat is omitted", async () => { const cwd = makeTempProject(); try { @@ -1345,7 +1343,7 @@ runtime = "deno" } }); - test("loads successfully with an invalid [remotes.*] project_id format when goViperCompat is omitted", async () => { + test("loads successfully with an invalid [remotes.*] project_id format when cliCompat is omitted", async () => { const cwd = makeTempProject(); try { @@ -1367,7 +1365,7 @@ project_id = "not-a-ref" } }); - test("does not split a comma-separated string literal for an array field when goViperCompat is omitted", async () => { + test("does not split a comma-separated string literal for an array field when cliCompat is omitted", async () => { const cwd = makeTempProject(); try { @@ -1398,7 +1396,7 @@ additional_redirect_urls = "http://a,http://b" } }); - test("does not warn on a deprecated provider (but still strips it) when goViperCompat is omitted", async () => { + test("does not warn on a deprecated provider (but still strips it) when cliCompat is omitted", async () => { const cwd = makeTempProject(); const warnings: Array = []; const errorSpy = vi.spyOn(console, "error").mockImplementation((...args) => { @@ -1425,7 +1423,7 @@ enabled = true } }); - test("does not resolve a lowercase-named env() reference when goViperCompat is omitted", async () => { + test("does not resolve a lowercase-named env() reference when cliCompat is omitted", async () => { const previous = process.env.lowercase_ref_default_off_test; process.env.lowercase_ref_default_off_test = "lowercase-ref-value"; const cwd = makeTempProject(); @@ -1449,7 +1447,7 @@ enabled = true } }); - test("resolves a lowercase-named env() reference when goViperCompat is true", async () => { + test("resolves a lowercase-named env() reference when cliCompat is true", async () => { const previous = process.env.lowercase_ref_default_on_test; process.env.lowercase_ref_default_on_test = "lowercase-ref-value"; const cwd = makeTempProject(); @@ -1461,7 +1459,7 @@ enabled = true `project_id = "env(lowercase_ref_default_on_test)"\n`, ); - const loaded = await runConfigEffect(loadCliConfig(cwd, { goViperCompat: true })); + const loaded = await runConfigEffect(loadCliConfig(cwd, { cliCompat: true })); expect(loaded!.config.project_id).toBe("lowercase-ref-value"); } finally { if (previous === undefined) { @@ -1938,7 +1936,7 @@ project_id = "dupref" `); try { const message = await Effect.runPromise( - loadCliConfig(cwd, { goViperCompat: true }).pipe( + loadCliConfig(cwd, { cliCompat: true }).pipe( Effect.catchTag("DuplicateRemoteProjectIdError", (error) => Effect.succeed(error.message), ), @@ -1951,8 +1949,8 @@ project_id = "dupref" } }); - // `goViperCompat` is required even though `projectRef` is passed: the duplicate/format checks - // are gated solely on `goViperCompat`, not on whether a remote is being selected. + // `cliCompat` is required even though `projectRef` is passed: the duplicate/format checks + // are gated solely on `cliCompat`, not on whether a remote is being selected. test("rejects duplicate project_id across remotes with Go's message", async () => { const cwd = await writeTomlProject(`project_id = "baseref" @@ -1964,7 +1962,7 @@ project_id = "dupref" `); try { const message = await Effect.runPromise( - loadCliConfig(cwd, { projectRef: "dupref", goViperCompat: true }).pipe( + loadCliConfig(cwd, { projectRef: "dupref", cliCompat: true }).pipe( Effect.catchTag("DuplicateRemoteProjectIdError", (error) => Effect.succeed(error.message), ), @@ -1993,7 +1991,7 @@ project_id = "dupref" `); try { const message = await Effect.runPromise( - loadCliConfig(cwd, { projectRef: "previewref", goViperCompat: true }).pipe( + loadCliConfig(cwd, { projectRef: "previewref", cliCompat: true }).pipe( Effect.catchTag("DuplicateRemoteProjectIdError", (error) => Effect.succeed(error.message), ), @@ -2021,7 +2019,7 @@ max_rows = 2 `); try { const message = await Effect.runPromise( - loadCliConfig(cwd, { projectRef: "previewref", goViperCompat: true }).pipe( + loadCliConfig(cwd, { projectRef: "previewref", cliCompat: true }).pipe( Effect.catchTag("DuplicateRemoteProjectIdError", (error) => Effect.succeed(error.message), ), @@ -2045,7 +2043,7 @@ project_id = "not-a-ref" `); try { const message = await Effect.runPromise( - loadCliConfig(cwd, { goViperCompat: true }).pipe( + loadCliConfig(cwd, { cliCompat: true }).pipe( Effect.catchTag("InvalidRemoteProjectIdError", (error) => Effect.succeed(error.message)), Effect.provide(BunServices.layer), ), @@ -2113,13 +2111,13 @@ enabled = true }); test("resolves env() on a lowercase-named variable, matching Go's case-agnostic matcher", async () => { - // Env-var matching is case-agnostic when `goViperCompat` is set; without it, the strict + // Env-var matching is case-agnostic when `cliCompat` is set; without it, the strict // SCREAMING_SNAKE_CASE matcher wouldn't match this lowercase name at all. const previous = process.env.project_id; process.env.project_id = "lowercase-ref"; const cwd = await writeTomlProject(`project_id = "env(project_id)"\n`); try { - const loaded = await runConfigEffect(loadCliConfig(cwd, { goViperCompat: true })); + const loaded = await runConfigEffect(loadCliConfig(cwd, { cliCompat: true })); expect(loaded!.config.project_id).toBe("lowercase-ref"); } finally { if (previous === undefined) { @@ -2646,7 +2644,7 @@ describe("config io deprecated [auth.external.{linkedin,slack}] back-compat", () [auth.external.slack] enabled = true `, - { goViperCompat: true }, + { cliCompat: true }, ); expect("slack" in loaded.config.auth.external).toBe(false); @@ -2668,7 +2666,7 @@ enabled = true [auth.external.linkedin] enabled = true `, - { goViperCompat: true }, + { cliCompat: true }, ); expect("linkedin" in loaded.config.auth.external).toBe(false); @@ -2915,7 +2913,7 @@ describe("decodeCliConfigDocumentForValidationEffect", () => { const config = await runConfigEffect( decodeCliConfigDocumentForValidationEffect( { project_id: "abc123" }, - { goViperCompat: true, path: "supabase/config.toml", format: "toml" }, + { cliCompat: true, path: "supabase/config.toml", format: "toml" }, ), ); expect(config.project_id).toBe("abc123"); @@ -2928,7 +2926,7 @@ describe("decodeCliConfigDocumentForValidationEffect", () => { }; const exit = await Effect.runPromiseExit( decodeCliConfigDocumentForValidationEffect(document, { - goViperCompat: true, + cliCompat: true, path: "supabase/config.toml", format: "toml", }).pipe(Effect.provide(BunServices.layer)), @@ -2960,7 +2958,7 @@ describe("decodeCliConfigDocumentForValidationEffect", () => { }; const config = await runConfigEffect( decodeCliConfigDocumentForValidationEffect(document, { - goViperCompat: true, + cliCompat: true, path: "supabase/config.toml", format: "toml", }), @@ -2985,7 +2983,7 @@ describe("decodeCliConfigDocumentForValidationEffect", () => { project_id: "abc123", db: { major_version: "env(SUPABASE_VALIDATION_PG_VERSION_TEST)" }, }, - { goViperCompat: true, path, format: "toml" }, + { cliCompat: true, path, format: "toml" }, ), ); expect(config.db.major_version).toBe(16); @@ -3013,7 +3011,7 @@ describe("decodeCliConfigDocumentForValidationEffect", () => { }; const config = await runConfigEffect( decodeCliConfigDocumentForValidationEffect(document, { - goViperCompat: true, + cliCompat: true, path: "supabase/config.toml", format: "toml", }), @@ -3033,7 +3031,7 @@ describe("decodeCliConfigDocumentForValidationEffect", () => { }; const exit = await Effect.runPromiseExit( decodeCliConfigDocumentForValidationEffect(document, { - goViperCompat: true, + cliCompat: true, path: "supabase/config.toml", format: "toml", remoteName: "staging", @@ -3064,7 +3062,7 @@ describe("decodeCliConfigDocumentForValidationEffect", () => { }; const config = await runConfigEffect( decodeCliConfigDocumentForValidationEffect(document, { - goViperCompat: true, + cliCompat: true, path: "supabase/config.toml", format: "toml", }), diff --git a/packages/config/src/lib/env.ts b/packages/config/src/lib/env.ts index 81ee02a363..fa38418baf 100644 --- a/packages/config/src/lib/env.ts +++ b/packages/config/src/lib/env.ts @@ -4,12 +4,12 @@ import { Schema, SchemaAST } from "effect"; // `env(project_id)` is a valid reference, not just SCREAMING_SNAKE_CASE names. export const ENV_PATTERN = "^env\\((.*)\\)$"; export const ENV_CAPTURE_REGEX = /^env\((.*)\)$/; -// Stricter matcher used when `goViperCompat` is off: only SCREAMING_SNAKE_CASE names match. +// Stricter matcher used when `cliCompat` is off: only SCREAMING_SNAKE_CASE names match. export const ENV_CAPTURE_REGEX_STRICT = /^env\(([A-Z_][A-Z0-9_]*)\)$/; const envRegex = new RegExp(ENV_PATTERN); -export function isEnvReference(value: string, goViperCompat: boolean): boolean { - return (goViperCompat ? ENV_CAPTURE_REGEX : ENV_CAPTURE_REGEX_STRICT).test(value); +export function isEnvReference(value: string, cliCompat: boolean): boolean { + return (cliCompat ? ENV_CAPTURE_REGEX : ENV_CAPTURE_REGEX_STRICT).test(value); } interface EnvAnnotations extends Schema.Annotations.Documentation { @@ -166,9 +166,9 @@ function coerceLeaf(value: unknown, expected: ExpectedType): unknown { function substituteEnvLeaf( value: string, env: Readonly>, - goViperCompat: boolean, + cliCompat: boolean, ): { readonly value: string; readonly resolved: boolean; readonly envName?: string } { - const match = (goViperCompat ? ENV_CAPTURE_REGEX : ENV_CAPTURE_REGEX_STRICT).exec(value); + const match = (cliCompat ? ENV_CAPTURE_REGEX : ENV_CAPTURE_REGEX_STRICT).exec(value); if (match === null) { return { value, resolved: false }; } @@ -203,7 +203,7 @@ function walk( document: unknown, env: Readonly>, ast: SchemaAST.AST | null, - goViperCompat: boolean, + cliCompat: boolean, path: ReadonlyArray, onResolvedEnv: | ((path: ReadonlyArray, envNames: ReadonlyArray) => void) @@ -225,7 +225,7 @@ function walk( }; const result = document.map((item, index) => { const child = ast === null ? null : descendAst(ast, String(index)); - return walk(item, env, child, goViperCompat, [...path, String(index)], onResolvedArrayEnv); + return walk(item, env, child, cliCompat, [...path, String(index)], onResolvedArrayEnv); }); if (envNames.length > 0) { onResolvedEnv?.(path, envNames); @@ -237,7 +237,7 @@ function walk( const result: Record = {}; for (const [key, value] of Object.entries(document)) { const child = ast === null ? null : descendAst(ast, key); - result[key] = walk(value, env, child, goViperCompat, [...path, key], onResolvedEnv); + result[key] = walk(value, env, child, cliCompat, [...path, key], onResolvedEnv); } return result; } @@ -250,7 +250,7 @@ function walk( return document; } - const interpolation = substituteEnvLeaf(document, env, goViperCompat); + const interpolation = substituteEnvLeaf(document, env, cliCompat); const substituted = interpolation.value; if (interpolation.resolved && interpolation.envName !== undefined) { onResolvedEnv?.(path, [interpolation.envName]); @@ -258,10 +258,10 @@ function walk( const expected = ast === null ? "unknown" : leafExpectedType(ast); // Unlike number/boolean coercion, array coercion also applies to literal strings that never - // went through env() substitution (e.g. plain TOML `"a,b"`). Gated by `goViperCompat`: off + // went through env() substitution (e.g. plain TOML `"a,b"`). Gated by `cliCompat`: off // leaves strings unsplit, so an array-typed field fed a string fails decode instead of coercing. if (expected === "array") { - return goViperCompat ? coerceLeaf(substituted, expected) : substituted; + return cliCompat ? coerceLeaf(substituted, expected) : substituted; } // Only the substituted form is fed to coercion; literal strings at non-string paths are @@ -288,19 +288,12 @@ export function interpolateEnvReferencesAgainstSchema( env: Readonly>, schema: { readonly ast: SchemaAST.AST }, options?: { - readonly goViperCompat?: boolean; + readonly cliCompat?: boolean; /** Fires per resolved leaf with the substituting env vars' names (array * leaves report once at the array path, collecting every element's * variable — one array literal may draw on several). */ readonly onResolvedEnv?: (path: ReadonlyArray, envNames: ReadonlyArray) => void; }, ): unknown { - return walk( - document, - env, - schema.ast, - options?.goViperCompat ?? false, - [], - options?.onResolvedEnv, - ); + return walk(document, env, schema.ast, options?.cliCompat ?? false, [], options?.onResolvedEnv); } diff --git a/packages/config/src/lib/resolve.ts b/packages/config/src/lib/resolve.ts index ef4d38d85d..f8d394cca6 100644 --- a/packages/config/src/lib/resolve.ts +++ b/packages/config/src/lib/resolve.ts @@ -30,9 +30,9 @@ export function toPathSegments(path: string): ReadonlyArray { function interpolateLeafValue( value: string, env: Readonly>, - goViperCompat: boolean, + cliCompat: boolean, ): string { - const match = (goViperCompat ? ENV_CAPTURE_REGEX : ENV_CAPTURE_REGEX_STRICT).exec(value); + const match = (cliCompat ? ENV_CAPTURE_REGEX : ENV_CAPTURE_REGEX_STRICT).exec(value); const envName = match?.[1]; if (envName === undefined) { @@ -57,45 +57,45 @@ function interpolateLeafValue( function interpolateValue( value: unknown, env: Readonly>, - goViperCompat: boolean, + cliCompat: boolean, ): unknown { if (Array.isArray(value)) { - return value.map((item) => interpolateValue(item, env, goViperCompat)); + return value.map((item) => interpolateValue(item, env, cliCompat)); } if (typeof value === "object" && value !== null) { const result: Record = {}; for (const [key, child] of Object.entries(value)) { - result[key] = interpolateValue(child, env, goViperCompat); + result[key] = interpolateValue(child, env, cliCompat); } return result; } if (typeof value === "string") { - return interpolateLeafValue(value, env, goViperCompat); + return interpolateLeafValue(value, env, cliCompat); } return value; } -function redactValue(value: unknown, path: ReadonlyArray, goViperCompat: boolean): unknown { +function redactValue(value: unknown, path: ReadonlyArray, cliCompat: boolean): unknown { if (Array.isArray(value)) { - return value.map((item, index) => redactValue(item, [...path, String(index)], goViperCompat)); + return value.map((item, index) => redactValue(item, [...path, String(index)], cliCompat)); } if (typeof value === "object" && value !== null) { const result: Record = {}; for (const [key, child] of Object.entries(value)) { - result[key] = redactValue(child, [...path, key], goViperCompat); + result[key] = redactValue(child, [...path, key], cliCompat); } return result; } - if (typeof value === "string" && isSecretPath(path) && !isEnvReference(value, goViperCompat)) { + if (typeof value === "string" && isSecretPath(path) && !isEnvReference(value, cliCompat)) { return Redacted.make(value, { label: path.join(".") }); } @@ -111,21 +111,21 @@ export function resolveCliConfigValueAtPath( value: T, cliProjectEnv: Pick, path: ReadonlyArray, - goViperCompat: boolean, + cliCompat: boolean, ): ResolvedCliConfigValue; export function resolveCliConfigValueAtPath( value: unknown, cliProjectEnv: Pick, path: ReadonlyArray, - goViperCompat: boolean, + cliCompat: boolean, ): unknown { - const interpolated = interpolateValue(value, cliProjectEnv.values, goViperCompat); - return redactValue(interpolated, path, goViperCompat); + const interpolated = interpolateValue(value, cliProjectEnv.values, cliCompat); + return redactValue(interpolated, path, cliCompat); } /** * Plain synchronous counterpart to the Effect-typed `resolveCliConfigValue` in `../project.ts`; - * has no options parameter, since this package's only resolver knob (`goViperCompat`) is + * has no options parameter, since this package's only resolver knob (`cliCompat`) is * internal-only. */ export function resolveCliConfigValue( diff --git a/packages/config/src/project.ts b/packages/config/src/project.ts index 42819fb68b..427002ca34 100644 --- a/packages/config/src/project.ts +++ b/packages/config/src/project.ts @@ -175,10 +175,10 @@ export interface LoadCliProjectEnvironmentOptions { /** Not covered by semver — exported from `@supabase/config/internal` only. */ export interface InternalResolveCliConfigOptions { /** - * Opt into viper-style case-agnostic `env()` matching (`^env\((.*)\)$`). Defaults to + * Opt into the CLI's case-agnostic `env()` matching (`^env\((.*)\)$`). Defaults to * `false`, which requires SCREAMING_SNAKE_CASE. */ - readonly goViperCompat?: boolean; + readonly cliCompat?: boolean; } export const loadCliProjectEnvironment = Effect.fn("ProjectEnvironment.load")(function* ( @@ -221,7 +221,7 @@ export const loadCliProjectEnvironment = Effect.fn("ProjectEnvironment.load")(fu /** * Effect-typed counterpart of the plain sync `resolveCliConfigValue` in - * `./lib/resolve.ts`, additionally accepting the internal-only `goViperCompat` option. + * `./lib/resolve.ts`, additionally accepting the internal-only `cliCompat` option. * * Accepts `Pick` so a caller with only a project's env * values, not the full loaded object, can pass `{ values }` directly. @@ -237,7 +237,7 @@ export function resolveCliConfigValue( value, cliProjectEnv, toPathSegments(configPath), - options?.goViperCompat ?? false, + options?.cliCompat ?? false, ), ); } @@ -254,7 +254,7 @@ export function resolveCliConfigSubtree( value, cliProjectEnv, toPathSegments(pathPrefix), - options?.goViperCompat ?? false, + options?.cliCompat ?? false, ), ); } diff --git a/packages/config/src/project.unit.test.ts b/packages/config/src/project.unit.test.ts index f57957c43c..4b44e677f3 100644 --- a/packages/config/src/project.unit.test.ts +++ b/packages/config/src/project.unit.test.ts @@ -568,7 +568,7 @@ jwt_secret = "env(lowercase_secret)" } }); - test("resolveCliConfigValue resolves a lowercase-named env() reference when goViperCompat is true", async () => { + test("resolveCliConfigValue resolves a lowercase-named env() reference when cliCompat is true", async () => { const cwd = makeTempProject(); const projectRoot = join(cwd, "repo"); @@ -592,7 +592,7 @@ jwt_secret = "env(lowercase_secret)" loaded!.config.auth.jwt_secret, projectEnv!, "auth.jwt_secret", - { goViperCompat: true }, + { cliCompat: true }, ), ); From 5dfeda75acece164ebe6cacb8c8ff5f6770a36ab Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Thu, 8 Oct 2026 13:55:22 +0100 Subject: [PATCH 2/9] refactor(cli): drop viper naming from SUPABASE_* env helpers Rename viper-env.ts to supabase-env.ts, viperEnv* to supabaseEnv*, and profile-load's VIPER_SUPPORTED_EXTS to PROFILE_FILE_EXTS. No behaviour change. Co-Authored-By: Claude Opus 5.5 --- .../db-bootstrap/local-container-inputs.ts | 4 +- .../db-bootstrap/shadow-cache.ts | 8 +- .../command-internal/docker-ids.unit.test.ts | 6 +- apps/cli/src/command-internal/global-flags.ts | 16 +-- apps/cli/src/command-internal/pg-dump.run.ts | 7 +- apps/cli/src/command-internal/profile-load.ts | 4 +- .../command-internal/stack-shadow-cache.ts | 4 +- .../{viper-env.ts => supabase-env.ts} | 18 +-- .../supabase-env.unit.test.ts | 121 ++++++++++++++++++ .../command-internal/viper-env.unit.test.ts | 121 ------------------ apps/cli/src/commands/db/dump/dump.handler.ts | 4 +- apps/cli/src/commands/start/start.handler.ts | 4 +- apps/cli/src/shared/functions/deploy.ts | 7 +- apps/cli/src/shared/functions/download.ts | 4 +- apps/cli/src/shared/functions/serve.ts | 4 +- 15 files changed, 169 insertions(+), 163 deletions(-) rename apps/cli/src/command-internal/{viper-env.ts => supabase-env.ts} (74%) create mode 100644 apps/cli/src/command-internal/supabase-env.unit.test.ts delete mode 100644 apps/cli/src/command-internal/viper-env.unit.test.ts diff --git a/apps/cli/src/command-internal/db-bootstrap/local-container-inputs.ts b/apps/cli/src/command-internal/db-bootstrap/local-container-inputs.ts index ce589339a3..e0a320cfe9 100644 --- a/apps/cli/src/command-internal/db-bootstrap/local-container-inputs.ts +++ b/apps/cli/src/command-internal/db-bootstrap/local-container-inputs.ts @@ -20,7 +20,7 @@ import { DbConfigLoadError } from "../db-config.errors.ts"; import { InvalidServiceVersionTagError } from "../service-version-overrides.ts"; import { localDbContainerId } from "../docker-ids.ts"; import { resolveDockerNetworkMode } from "../../shared/functions/functions-docker.ts"; -import { viperEnvStringWithProjectFallback } from "../viper-env.ts"; +import { supabaseEnvStringWithProjectFallback } from "../supabase-env.ts"; import { isBitbucketPipeline } from "../bitbucket-pipeline.ts"; import { resolveAuthExternalUrl, @@ -136,7 +136,7 @@ export const buildLocalDbContainerInputs = ( // See {@link resolveDockerNetworkMode} for the full flag/env/fallback precedence. const networkId = resolveDockerNetworkMode({ explicit: Option.getOrUndefined(networkIdFlag), - envOverride: viperEnvStringWithProjectFallback("SUPABASE_NETWORK_ID", projectEnvValues), + envOverride: supabaseEnvStringWithProjectFallback("SUPABASE_NETWORK_ID", projectEnvValues), projectId, }); // Only needed on Linux; Docker Desktop already resolves `host.docker.internal` elsewhere. diff --git a/apps/cli/src/command-internal/db-bootstrap/shadow-cache.ts b/apps/cli/src/command-internal/db-bootstrap/shadow-cache.ts index 1ffe855cc4..3a64dfddfa 100644 --- a/apps/cli/src/command-internal/db-bootstrap/shadow-cache.ts +++ b/apps/cli/src/command-internal/db-bootstrap/shadow-cache.ts @@ -20,7 +20,7 @@ import { } from "effect"; import type { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"; -import { viperEnvBoolWithProjectFallback } from "../viper-env.ts"; +import { supabaseEnvBoolWithProjectFallback } from "../supabase-env.ts"; import { Output } from "../../shared/output/output.service.ts"; import { containerCliExitCode, describeContainerCliFailure } from "../container-cli.ts"; import { DbConnection } from "../db-connection.service.ts"; @@ -60,7 +60,7 @@ import { type Spawner = ChildProcessSpawner["Service"]; -/** `SUPABASE_SHADOW_CACHE` — opt-out gate (viper bool when set; unset is ON). */ +/** `SUPABASE_SHADOW_CACHE` — opt-out gate (parsed as a boolean when set; unset is ON). */ export const SHADOW_CACHE_ENV = "SUPABASE_SHADOW_CACHE"; /** @@ -688,7 +688,7 @@ export const peekShadowBaseline = ( Effect.gen(function* () { if ( opts.bypassCache === true || - !viperEnvBoolWithProjectFallback(SHADOW_CACHE_ENV, input.setup.projectEnvValues ?? {}) + !supabaseEnvBoolWithProjectFallback(SHADOW_CACHE_ENV, input.setup.projectEnvValues ?? {}) ) { return { state: "uncachable" } as const; } @@ -868,7 +868,7 @@ export const acquireShadowDatabase = ( Effect.gen(function* () { if ( opts.bypassCache === true || - !viperEnvBoolWithProjectFallback(SHADOW_CACHE_ENV, input.setup.projectEnvValues ?? {}, { + !supabaseEnvBoolWithProjectFallback(SHADOW_CACHE_ENV, input.setup.projectEnvValues ?? {}, { whenUnset: true, }) ) { diff --git a/apps/cli/src/command-internal/docker-ids.unit.test.ts b/apps/cli/src/command-internal/docker-ids.unit.test.ts index 701e0740a6..c1c748b1a5 100644 --- a/apps/cli/src/command-internal/docker-ids.unit.test.ts +++ b/apps/cli/src/command-internal/docker-ids.unit.test.ts @@ -10,7 +10,7 @@ import { localNetworkId, } from "./docker-ids.ts"; import { resolveDockerNetworkMode } from "../shared/functions/functions-docker.ts"; -import { viperEnvStringWithProjectFallback } from "./viper-env.ts"; +import { supabaseEnvStringWithProjectFallback } from "./supabase-env.ts"; describe("resolveLocalProjectId", () => { it("prefers SUPABASE_PROJECT_ID (env) over config.toml and the basename", () => { @@ -74,7 +74,7 @@ describe("cliProjectFilterValue", () => { }); }); -describe("resolveDockerNetworkMode composed with viperEnvStringWithProjectFallback (start/db start call shape)", () => { +describe("resolveDockerNetworkMode composed with supabaseEnvStringWithProjectFallback (start/db start call shape)", () => { const KEY = "SUPABASE_NETWORK_ID"; afterEach(() => { @@ -84,7 +84,7 @@ describe("resolveDockerNetworkMode composed with viperEnvStringWithProjectFallba function resolve(flagValue: string | undefined, projectEnv: Record) { return resolveDockerNetworkMode({ explicit: flagValue, - envOverride: viperEnvStringWithProjectFallback(KEY, projectEnv), + envOverride: supabaseEnvStringWithProjectFallback(KEY, projectEnv), projectId: "my-app", }); } diff --git a/apps/cli/src/command-internal/global-flags.ts b/apps/cli/src/command-internal/global-flags.ts index df8b051ac9..244ef7e38c 100644 --- a/apps/cli/src/command-internal/global-flags.ts +++ b/apps/cli/src/command-internal/global-flags.ts @@ -3,7 +3,7 @@ import { Flag, GlobalFlag } from "effect/unstable/cli"; import { CliArgs } from "../shared/cli/cli-args.service.ts"; import { VALUE_CONSUMING_LONG_FLAGS, VALUE_CONSUMING_SHORT_FLAGS } from "./db-target-flags.ts"; -import { viperEnvBool, viperEnvBoolWithProjectFallback } from "./viper-env.ts"; +import { supabaseEnvBool, supabaseEnvBoolWithProjectFallback } from "./supabase-env.ts"; // The CLI's global-flag registry is tree-wide, so `-o/--output` can't be redeclared per command to // vary its allowed values; this models it as the union of every command's accepted values, and @@ -228,14 +228,14 @@ export const resolveYes = Effect.gen(function* () { if (yesFlagExplicitlyFalse(cliArgs.args)) { return false; } - return flag || viperEnvBool("SUPABASE_YES"); + return flag || supabaseEnvBool("SUPABASE_YES"); }); /** * `--yes` resolved with the project `.env` consulted too, for commands that load the nested * project env before prompting (`migration down`, `migration repair --all`). Shell env * *presence* (any value) suppresses the file value entirely (see - * {@link viperEnvBoolWithProjectFallback}); an explicit `--yes` wins over both. `projectEnv` is + * {@link supabaseEnvBoolWithProjectFallback}); an explicit `--yes` wins over both. `projectEnv` is * the loaded map from `loadProjectEnv`. */ export const resolveYesWithProjectEnv = (projectEnv: Record) => @@ -245,7 +245,7 @@ export const resolveYesWithProjectEnv = (projectEnv: Record) => if (yesFlagExplicitlyFalse(cliArgs.args)) { return false; } - return flag || viperEnvBoolWithProjectFallback("SUPABASE_YES", projectEnv); + return flag || supabaseEnvBoolWithProjectFallback("SUPABASE_YES", projectEnv); }); /** @@ -279,12 +279,12 @@ export const resolveExperimental = Effect.gen(function* () { if (explicit !== undefined) { return explicit; } - return flag || viperEnvBool("SUPABASE_EXPERIMENTAL"); + return flag || supabaseEnvBool("SUPABASE_EXPERIMENTAL"); }); /** * `--experimental` with project `.env` fallback. Shell-env presence wins over the file; - * an explicit flag wins over both. See {@link viperEnvBoolWithProjectFallback}. + * an explicit flag wins over both. See {@link supabaseEnvBoolWithProjectFallback}. */ export const resolveExperimentalWithProjectEnv = (projectEnv: Record) => Effect.gen(function* () { @@ -294,7 +294,7 @@ export const resolveExperimentalWithProjectEnv = (projectEnv: Record) = if (debugFlagExplicitlyFalse(cliArgs.args)) { return false; } - return flag || viperEnvBoolWithProjectFallback("SUPABASE_DEBUG", projectEnv); + return flag || supabaseEnvBoolWithProjectFallback("SUPABASE_DEBUG", projectEnv); }); diff --git a/apps/cli/src/command-internal/pg-dump.run.ts b/apps/cli/src/command-internal/pg-dump.run.ts index 8e50a6cf6c..1bfe0ea920 100644 --- a/apps/cli/src/command-internal/pg-dump.run.ts +++ b/apps/cli/src/command-internal/pg-dump.run.ts @@ -6,7 +6,7 @@ type StackRuntimePreference = | { readonly kind: "container"; readonly engine: "docker" | "podman" }; import { NetworkIdFlag } from "./global-flags.ts"; -import { viperEnvStringWithProjectFallback } from "./viper-env.ts"; +import { supabaseEnvStringWithProjectFallback } from "./supabase-env.ts"; import { RuntimeInfo } from "../shared/runtime/runtime-info.service.ts"; import { DockerRun } from "./docker-run.service.ts"; import { DockerRunError } from "./docker-run.errors.ts"; @@ -113,7 +113,10 @@ export const dumpNetworkMode = ( ): { readonly _tag: "named"; readonly name: string } | { readonly _tag: "host" } => { if (networkId !== undefined && networkId.length > 0) return { _tag: "named", name: networkId }; if (forceHostNetwork) return { _tag: "host" }; - const envNetworkId = viperEnvStringWithProjectFallback("SUPABASE_NETWORK_ID", projectEnvValues); + const envNetworkId = supabaseEnvStringWithProjectFallback( + "SUPABASE_NETWORK_ID", + projectEnvValues, + ); return envNetworkId.length > 0 ? { _tag: "named", name: envNetworkId } : { _tag: "host" }; }; diff --git a/apps/cli/src/command-internal/profile-load.ts b/apps/cli/src/command-internal/profile-load.ts index 28a1c66c65..58d2c43d29 100644 --- a/apps/cli/src/command-internal/profile-load.ts +++ b/apps/cli/src/command-internal/profile-load.ts @@ -70,7 +70,7 @@ export function loadProfile( } const ext = goFilepathExt(token); - if (!VIPER_SUPPORTED_EXTS.has(ext)) { + if (!PROFILE_FILE_EXTS.has(ext)) { return yield* failRead(`Unsupported Config Type ${JSON.stringify(ext)}`); } @@ -177,7 +177,7 @@ const failDecode = (detail: string) => ); /** Recognized config file extensions, checked case-sensitively. */ -const VIPER_SUPPORTED_EXTS: ReadonlySet = new Set([ +const PROFILE_FILE_EXTS: ReadonlySet = new Set([ "json", "toml", "yaml", diff --git a/apps/cli/src/command-internal/stack-shadow-cache.ts b/apps/cli/src/command-internal/stack-shadow-cache.ts index d53aec24de..340f7ba26f 100644 --- a/apps/cli/src/command-internal/stack-shadow-cache.ts +++ b/apps/cli/src/command-internal/stack-shadow-cache.ts @@ -7,7 +7,7 @@ import { type ShadowCacheKeyInputs, } from "./db-bootstrap/shadow-cache.ts"; import { resolveSetupWebhooksEnabled, type SetupDatabaseOptions } from "./db-bootstrap/db-setup.ts"; -import { viperEnvBoolWithProjectFallback } from "./viper-env.ts"; +import { supabaseEnvBoolWithProjectFallback } from "./supabase-env.ts"; interface StackShadowCacheEntry { readonly key: string; @@ -31,7 +31,7 @@ export const stackShadowCacheEntry = Effect.fn("StackShadowCache.entry")(functio ) { if ( bypassCache || - !viperEnvBoolWithProjectFallback(SHADOW_CACHE_ENV, input.setup.projectEnvValues ?? {}, { + !supabaseEnvBoolWithProjectFallback(SHADOW_CACHE_ENV, input.setup.projectEnvValues ?? {}, { whenUnset: true, }) ) diff --git a/apps/cli/src/command-internal/viper-env.ts b/apps/cli/src/command-internal/supabase-env.ts similarity index 74% rename from apps/cli/src/command-internal/viper-env.ts rename to apps/cli/src/command-internal/supabase-env.ts index 01bc4257d7..0bf2ab15d1 100644 --- a/apps/cli/src/command-internal/viper-env.ts +++ b/apps/cli/src/command-internal/supabase-env.ts @@ -6,16 +6,16 @@ * so callers OR the parsed flag value with this read (flag-set wins). */ -const VIPER_TRUE = new Set(["1", "t", "T", "TRUE", "true", "True"]); +const TRUE_VALUES = new Set(["1", "t", "T", "TRUE", "true", "True"]); /** Truthiness for an already-resolved env value (see module doc). */ -function viperBool(raw: string | undefined): boolean { - return raw !== undefined && VIPER_TRUE.has(raw); +function parseEnvBool(raw: string | undefined): boolean { + return raw !== undefined && TRUE_VALUES.has(raw); } /** Reads a single `SUPABASE_*` boolean env var from `process.env` (see module doc). */ -export function viperEnvBool(name: string): boolean { - return viperBool(process.env[name]); +export function supabaseEnvBool(name: string): boolean { + return parseEnvBool(process.env[name]); } /** @@ -26,23 +26,23 @@ export function viperEnvBool(name: string): boolean { * project env, letting an opt-out gate default on while any present value * still disables. */ -export function viperEnvBoolWithProjectFallback( +export function supabaseEnvBoolWithProjectFallback( name: string, projectEnv: Record, opts: { readonly whenUnset?: boolean } = {}, ): boolean { const raw = process.env[name] ?? projectEnv[name]; if (raw === undefined) return opts.whenUnset ?? false; - return viperBool(raw); + return parseEnvBool(raw); } /** * Resolves a `SUPABASE_*` string with the same shell-presence-wins semantics - * as {@link viperEnvBoolWithProjectFallback}, but with no boolean coercion — + * as {@link supabaseEnvBoolWithProjectFallback}, but with no boolean coercion — * the raw merged string, or `""` when absent from both. `??` (not `||`) * encodes the presence check. */ -export function viperEnvStringWithProjectFallback( +export function supabaseEnvStringWithProjectFallback( name: string, projectEnv: Record, ): string { diff --git a/apps/cli/src/command-internal/supabase-env.unit.test.ts b/apps/cli/src/command-internal/supabase-env.unit.test.ts new file mode 100644 index 0000000000..0c9836699a --- /dev/null +++ b/apps/cli/src/command-internal/supabase-env.unit.test.ts @@ -0,0 +1,121 @@ +import { afterEach, describe, expect, it } from "vitest"; + +import { + supabaseEnvBool, + supabaseEnvBoolWithProjectFallback, + supabaseEnvStringWithProjectFallback, +} from "./supabase-env.ts"; + +const KEY = "SUPABASE_TEST_ENV_BOOL"; +const STRING_KEY = "SUPABASE_TEST_ENV_STRING"; + +describe("supabaseEnvBool", () => { + afterEach(() => { + delete process.env[KEY]; + }); + + it("is true only for strconv.ParseBool's true set", () => { + for (const value of ["1", "t", "T", "TRUE", "true", "True"]) { + process.env[KEY] = value; + expect(supabaseEnvBool(KEY)).toBe(true); + } + }); + + it("is false for the false set and any unrecognized value", () => { + for (const value of ["0", "f", "F", "FALSE", "false", "False", "yes", "on", "", "nope"]) { + process.env[KEY] = value; + expect(supabaseEnvBool(KEY)).toBe(false); + } + }); + + it("is false when the env var is absent", () => { + delete process.env[KEY]; + expect(supabaseEnvBool(KEY)).toBe(false); + }); +}); + +describe("supabaseEnvBoolWithProjectFallback", () => { + afterEach(() => { + delete process.env[KEY]; + }); + + it("falls back to the project value only when the shell var is absent", () => { + delete process.env[KEY]; + expect(supabaseEnvBoolWithProjectFallback(KEY, { [KEY]: "true" })).toBe(true); + expect(supabaseEnvBoolWithProjectFallback(KEY, { [KEY]: "false" })).toBe(false); + expect(supabaseEnvBoolWithProjectFallback(KEY, {})).toBe(false); + }); + + it("keeps a false shell override even when the project .env says true", () => { + process.env[KEY] = "false"; + expect(supabaseEnvBoolWithProjectFallback(KEY, { [KEY]: "true" })).toBe(false); + }); + + it("treats an empty shell value as present (blocks the project value) and false", () => { + process.env[KEY] = ""; + expect(supabaseEnvBoolWithProjectFallback(KEY, { [KEY]: "true" })).toBe(false); + }); + + it("treats an unparsable shell value as present and false (cast.ToBool swallows the error)", () => { + process.env[KEY] = "banana"; + expect(supabaseEnvBoolWithProjectFallback(KEY, { [KEY]: "true" })).toBe(false); + }); + + it("keeps a true shell value over a false project value", () => { + process.env[KEY] = "true"; + expect(supabaseEnvBoolWithProjectFallback(KEY, { [KEY]: "false" })).toBe(true); + }); + + it("whenUnset: true resolves a key absent from both envs to true (opt-out gate default)", () => { + delete process.env[KEY]; + expect(supabaseEnvBoolWithProjectFallback(KEY, {}, { whenUnset: true })).toBe(true); + }); + + it("whenUnset: true still yields false for any present non-true value", () => { + process.env[KEY] = "0"; + expect(supabaseEnvBoolWithProjectFallback(KEY, {}, { whenUnset: true })).toBe(false); + process.env[KEY] = ""; + expect(supabaseEnvBoolWithProjectFallback(KEY, { [KEY]: "true" }, { whenUnset: true })).toBe( + false, + ); + process.env[KEY] = "banana"; + expect(supabaseEnvBoolWithProjectFallback(KEY, {}, { whenUnset: true })).toBe(false); + delete process.env[KEY]; + expect(supabaseEnvBoolWithProjectFallback(KEY, { [KEY]: "false" }, { whenUnset: true })).toBe( + false, + ); + }); +}); + +describe("supabaseEnvStringWithProjectFallback", () => { + afterEach(() => { + delete process.env[STRING_KEY]; + }); + + it("falls back to the project value only when the shell var is absent", () => { + delete process.env[STRING_KEY]; + expect( + supabaseEnvStringWithProjectFallback(STRING_KEY, { [STRING_KEY]: "project-value" }), + ).toBe("project-value"); + expect(supabaseEnvStringWithProjectFallback(STRING_KEY, {})).toBe(""); + }); + + it("keeps the shell value over a project value", () => { + process.env[STRING_KEY] = "shell-value"; + expect( + supabaseEnvStringWithProjectFallback(STRING_KEY, { [STRING_KEY]: "project-value" }), + ).toBe("shell-value"); + }); + + it("treats an empty shell value as present (blocks the project value)", () => { + process.env[STRING_KEY] = ""; + expect( + supabaseEnvStringWithProjectFallback(STRING_KEY, { [STRING_KEY]: "project-value" }), + ).toBe(""); + }); + + it("returns an empty string (not undefined) when absent from both", () => { + delete process.env[STRING_KEY]; + expect(supabaseEnvStringWithProjectFallback(STRING_KEY, {})).toBe(""); + }); +}); diff --git a/apps/cli/src/command-internal/viper-env.unit.test.ts b/apps/cli/src/command-internal/viper-env.unit.test.ts deleted file mode 100644 index 019307500e..0000000000 --- a/apps/cli/src/command-internal/viper-env.unit.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { afterEach, describe, expect, it } from "vitest"; - -import { - viperEnvBool, - viperEnvBoolWithProjectFallback, - viperEnvStringWithProjectFallback, -} from "./viper-env.ts"; - -const KEY = "SUPABASE_TEST_VIPER_BOOL"; -const STRING_KEY = "SUPABASE_TEST_VIPER_STRING"; - -describe("viperEnvBool", () => { - afterEach(() => { - delete process.env[KEY]; - }); - - it("is true only for strconv.ParseBool's true set (viper.GetBool parity)", () => { - for (const value of ["1", "t", "T", "TRUE", "true", "True"]) { - process.env[KEY] = value; - expect(viperEnvBool(KEY)).toBe(true); - } - }); - - it("is false for the false set and any unrecognized value", () => { - for (const value of ["0", "f", "F", "FALSE", "false", "False", "yes", "on", "", "nope"]) { - process.env[KEY] = value; - expect(viperEnvBool(KEY)).toBe(false); - } - }); - - it("is false when the env var is absent", () => { - delete process.env[KEY]; - expect(viperEnvBool(KEY)).toBe(false); - }); -}); - -describe("viperEnvBoolWithProjectFallback", () => { - afterEach(() => { - delete process.env[KEY]; - }); - - it("falls back to the project value only when the shell var is absent", () => { - delete process.env[KEY]; - expect(viperEnvBoolWithProjectFallback(KEY, { [KEY]: "true" })).toBe(true); - expect(viperEnvBoolWithProjectFallback(KEY, { [KEY]: "false" })).toBe(false); - expect(viperEnvBoolWithProjectFallback(KEY, {})).toBe(false); - }); - - it("keeps a false shell override even when the project .env says true", () => { - process.env[KEY] = "false"; - expect(viperEnvBoolWithProjectFallback(KEY, { [KEY]: "true" })).toBe(false); - }); - - it("treats an empty shell value as present (blocks the project value) and false", () => { - process.env[KEY] = ""; - expect(viperEnvBoolWithProjectFallback(KEY, { [KEY]: "true" })).toBe(false); - }); - - it("treats an unparsable shell value as present and false (cast.ToBool swallows the error)", () => { - process.env[KEY] = "banana"; - expect(viperEnvBoolWithProjectFallback(KEY, { [KEY]: "true" })).toBe(false); - }); - - it("keeps a true shell value over a false project value", () => { - process.env[KEY] = "true"; - expect(viperEnvBoolWithProjectFallback(KEY, { [KEY]: "false" })).toBe(true); - }); - - it("whenUnset: true resolves a key absent from both envs to true (opt-out gate default)", () => { - delete process.env[KEY]; - expect(viperEnvBoolWithProjectFallback(KEY, {}, { whenUnset: true })).toBe(true); - }); - - it("whenUnset: true still yields false for any present non-true value", () => { - process.env[KEY] = "0"; - expect(viperEnvBoolWithProjectFallback(KEY, {}, { whenUnset: true })).toBe(false); - process.env[KEY] = ""; - expect(viperEnvBoolWithProjectFallback(KEY, { [KEY]: "true" }, { whenUnset: true })).toBe( - false, - ); - process.env[KEY] = "banana"; - expect(viperEnvBoolWithProjectFallback(KEY, {}, { whenUnset: true })).toBe(false); - delete process.env[KEY]; - expect(viperEnvBoolWithProjectFallback(KEY, { [KEY]: "false" }, { whenUnset: true })).toBe( - false, - ); - }); -}); - -describe("viperEnvStringWithProjectFallback", () => { - afterEach(() => { - delete process.env[STRING_KEY]; - }); - - it("falls back to the project value only when the shell var is absent", () => { - delete process.env[STRING_KEY]; - expect(viperEnvStringWithProjectFallback(STRING_KEY, { [STRING_KEY]: "project-value" })).toBe( - "project-value", - ); - expect(viperEnvStringWithProjectFallback(STRING_KEY, {})).toBe(""); - }); - - it("keeps the shell value over a project value", () => { - process.env[STRING_KEY] = "shell-value"; - expect(viperEnvStringWithProjectFallback(STRING_KEY, { [STRING_KEY]: "project-value" })).toBe( - "shell-value", - ); - }); - - it("treats an empty shell value as present (blocks the project value)", () => { - process.env[STRING_KEY] = ""; - expect(viperEnvStringWithProjectFallback(STRING_KEY, { [STRING_KEY]: "project-value" })).toBe( - "", - ); - }); - - it("returns an empty string (not undefined) when absent from both, matching viper.GetString", () => { - delete process.env[STRING_KEY]; - expect(viperEnvStringWithProjectFallback(STRING_KEY, {})).toBe(""); - }); -}); diff --git a/apps/cli/src/commands/db/dump/dump.handler.ts b/apps/cli/src/commands/db/dump/dump.handler.ts index c4d32a5dc9..7eb7a27f4a 100644 --- a/apps/cli/src/commands/db/dump/dump.handler.ts +++ b/apps/cli/src/commands/db/dump/dump.handler.ts @@ -49,7 +49,7 @@ import { stackRequireProjectRuntime, } from "../../../command-internal/stack-local-database.ts"; import { resolveBundledPostgresRuntime } from "../../../command-internal/bundled-postgres-client.ts"; -import { viperEnvStringWithProjectFallback } from "../../../command-internal/viper-env.ts"; +import { supabaseEnvStringWithProjectFallback } from "../../../command-internal/supabase-env.ts"; import { DockerRunError } from "../../../command-internal/docker-run.errors.ts"; import { runWithPoolerFallback } from "../shared/pooler-fallback.ts"; import { @@ -209,7 +209,7 @@ export const dbDump = Effect.fn("db.dump")(function* (flags: DbDumpFlags) { : undefined; const useNativeClient = bundledRuntime?.kind === "native"; const networkId = Option.getOrUndefined(networkIdFlag); - const envNetworkId = viperEnvStringWithProjectFallback("SUPABASE_NETWORK_ID", projectEnv); + const envNetworkId = supabaseEnvStringWithProjectFallback("SUPABASE_NETWORK_ID", projectEnv); const dumpUsesHostNetwork = backend.kind === "stack" ? toolContainerUsesHostNetwork(networkId) diff --git a/apps/cli/src/commands/start/start.handler.ts b/apps/cli/src/commands/start/start.handler.ts index 5efe0dd914..30971c1ff5 100644 --- a/apps/cli/src/commands/start/start.handler.ts +++ b/apps/cli/src/commands/start/start.handler.ts @@ -59,7 +59,7 @@ import { localDbContainerId, } from "../../command-internal/docker-ids.ts"; import { resolveDockerNetworkMode } from "../../shared/functions/functions-docker.ts"; -import { viperEnvStringWithProjectFallback } from "../../command-internal/viper-env.ts"; +import { supabaseEnvStringWithProjectFallback } from "../../command-internal/supabase-env.ts"; import { inspectContainerState, listContainersByLabel, @@ -842,7 +842,7 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { const networkIdFlag = yield* NetworkIdFlag; const networkId = resolveDockerNetworkMode({ explicit: Option.getOrUndefined(networkIdFlag), - envOverride: viperEnvStringWithProjectFallback("SUPABASE_NETWORK_ID", projectEnvValues), + envOverride: supabaseEnvStringWithProjectFallback("SUPABASE_NETWORK_ID", projectEnvValues), projectId, }); // Linux-only `host.docker.internal:host-gateway` extra host; empty on darwin/windows, where diff --git a/apps/cli/src/shared/functions/deploy.ts b/apps/cli/src/shared/functions/deploy.ts index 2898de467c..78420047c1 100644 --- a/apps/cli/src/shared/functions/deploy.ts +++ b/apps/cli/src/shared/functions/deploy.ts @@ -30,7 +30,7 @@ import { bitbucketCloneDir } from "../../command-internal/bitbucket-pipeline.ts" import { CONTEXT_CANCELED_MESSAGE } from "../output/errors.ts"; import { Output } from "../output/output.service.ts"; import { bold } from "../../command-internal/colors.ts"; -import { viperEnvStringWithProjectFallback } from "../../command-internal/viper-env.ts"; +import { supabaseEnvStringWithProjectFallback } from "../../command-internal/supabase-env.ts"; import { findGitRootPath } from "../git/git-root.ts"; import { cobraMutuallyExclusiveErrorMessage, @@ -2607,7 +2607,10 @@ export const deployFunctions = Effect.fn("functions.deploy")(function* < envOverride: context.projectEnvValues === undefined ? undefined - : viperEnvStringWithProjectFallback("SUPABASE_NETWORK_ID", context.projectEnvValues), + : supabaseEnvStringWithProjectFallback( + "SUPABASE_NETWORK_ID", + context.projectEnvValues, + ), projectId: context.projectId, }); yield* deployViaDocker({ diff --git a/apps/cli/src/shared/functions/download.ts b/apps/cli/src/shared/functions/download.ts index 5f2d69961b..5ab985f7f6 100644 --- a/apps/cli/src/shared/functions/download.ts +++ b/apps/cli/src/shared/functions/download.ts @@ -15,7 +15,7 @@ import { } from "../cli/cobra-flag-groups.ts"; import { describeContainerCliFailure } from "../../command-internal/container-cli.ts"; import { bitbucketCloneDir } from "../../command-internal/bitbucket-pipeline.ts"; -import { viperEnvStringWithProjectFallback } from "../../command-internal/viper-env.ts"; +import { supabaseEnvStringWithProjectFallback } from "../../command-internal/supabase-env.ts"; import { buildFunctionsDockerRunArgs, edgeRuntimeCacheVolume, @@ -985,7 +985,7 @@ const downloadWithDockerUnbundle = Effect.fn("functions.download.dockerUnbundle" envOverride: projectEnvValues === undefined ? undefined - : viperEnvStringWithProjectFallback("SUPABASE_NETWORK_ID", projectEnvValues), + : supabaseEnvStringWithProjectFallback("SUPABASE_NETWORK_ID", projectEnvValues), projectId, }); diff --git a/apps/cli/src/shared/functions/serve.ts b/apps/cli/src/shared/functions/serve.ts index 146ea74236..00087d489d 100644 --- a/apps/cli/src/shared/functions/serve.ts +++ b/apps/cli/src/shared/functions/serve.ts @@ -51,7 +51,7 @@ import { import { inspectContainerState } from "../../command-internal/docker-lifecycle.ts"; import { isDockerDaemonUnreachable } from "../../command-internal/docker-suggest.ts"; import { parseDotEnv } from "../../command-internal/dotenv.ts"; -import { viperEnvStringWithProjectFallback } from "../../command-internal/viper-env.ts"; +import { supabaseEnvStringWithProjectFallback } from "../../command-internal/supabase-env.ts"; import { resolveRemoteJwks, resolveThirdPartyIssuerUrl, @@ -2008,7 +2008,7 @@ const startEdgeRuntime = Effect.fn("functions.serve.startEdgeRuntime")(function* envOverride: resolved.projectEnvValues === undefined ? undefined - : viperEnvStringWithProjectFallback("SUPABASE_NETWORK_ID", resolved.projectEnvValues), + : supabaseEnvStringWithProjectFallback("SUPABASE_NETWORK_ID", resolved.projectEnvValues), projectId, }); const localAuthArtifacts = yield* resolveLocalAuthArtifacts(resolved.auth, resolved.configPath); From 10d3a9bb2ea4422a0b7702ed632fe1f517b0925a Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Thu, 8 Oct 2026 13:57:46 +0100 Subject: [PATCH 3/9] test(cli): drop viper references from test names and docs Test titles, comments, and SIDE_EFFECTS.md entries now describe the behaviour itself instead of citing viper. No behaviour change. Co-Authored-By: Claude Opus 5.5 --- .../db-config.toml-read.unit.test.ts | 6 +- .../command-internal/docker-ids.unit.test.ts | 2 +- .../experimental-gate.unit.test.ts | 72 +++++++++---------- .../go-output.encoders.unit.test.ts | 4 +- .../local-config-values.unit.test.ts | 10 +-- .../migration-apply.unit.test.ts | 2 +- .../pflag-reconcile.unit.test.ts | 2 +- .../profile-load.unit.test.ts | 40 +++++------ .../upgrade-notice.unit.test.ts | 4 +- apps/cli/src/commands/db/diff/SIDE_EFFECTS.md | 2 +- apps/cli/src/commands/db/dump/SIDE_EFFECTS.md | 18 ++--- apps/cli/src/commands/db/pull/SIDE_EFFECTS.md | 2 +- .../commands/db/pull/pull.integration.test.ts | 2 +- .../cli/src/commands/db/reset/SIDE_EFFECTS.md | 2 +- .../schema/declarative/sync/SIDE_EFFECTS.md | 2 +- .../network-restrictions/get/SIDE_EFFECTS.md | 2 +- apps/cli/src/commands/start/SIDE_EFFECTS.md | 2 +- apps/cli/src/commands/status/SIDE_EFFECTS.md | 2 +- apps/cli/src/shared/cli/cobra-flag-groups.ts | 2 +- 19 files changed, 85 insertions(+), 93 deletions(-) diff --git a/apps/cli/src/command-internal/db-config.toml-read.unit.test.ts b/apps/cli/src/command-internal/db-config.toml-read.unit.test.ts index 37e65f0246..ba48e1b839 100644 --- a/apps/cli/src/command-internal/db-config.toml-read.unit.test.ts +++ b/apps/cli/src/command-internal/db-config.toml-read.unit.test.ts @@ -2298,7 +2298,7 @@ describe("readDbToml", () => { ); }); - it.effect("lets SUPABASE_DB_* env vars override the [db] config (viper AutomaticEnv)", () => { + it.effect("lets SUPABASE_DB_* env vars override the [db] config", () => { const prev = { PORT: process.env["SUPABASE_DB_PORT"], SHADOW: process.env["SUPABASE_DB_SHADOW_PORT"], @@ -2525,7 +2525,7 @@ describe("readDbToml", () => { ); }); - it.effect("ignores an empty SUPABASE_DB_PORT override (viper AllowEmptyEnv=false)", () => { + it.effect("ignores an empty SUPABASE_DB_PORT override", () => { const prev = process.env["SUPABASE_DB_PORT"]; process.env["SUPABASE_DB_PORT"] = ""; const dir = withConfig(["[db]", "port = 55555", ""].join("\n")); @@ -3241,7 +3241,7 @@ describe("readDbToml SUPABASE_PROJECT_ID override (Go AutomaticEnv parity)", () ); }); - it.effect("ignores an empty SUPABASE_PROJECT_ID (viper AllowEmptyEnv=false)", () => { + it.effect("ignores an empty SUPABASE_PROJECT_ID", () => { const previous = process.env["SUPABASE_PROJECT_ID"]; process.env["SUPABASE_PROJECT_ID"] = ""; const dir = withConfig(['project_id = "toml-project"', ""].join("\n")); diff --git a/apps/cli/src/command-internal/docker-ids.unit.test.ts b/apps/cli/src/command-internal/docker-ids.unit.test.ts index c1c748b1a5..f4c587702e 100644 --- a/apps/cli/src/command-internal/docker-ids.unit.test.ts +++ b/apps/cli/src/command-internal/docker-ids.unit.test.ts @@ -115,7 +115,7 @@ describe("resolveDockerNetworkMode composed with supabaseEnvStringWithProjectFal expect(resolve("", {})).toBe(localNetworkId("my-app")); }); - it("an explicit-but-empty --network-id= skips the env var entirely (viper: a Changed pflag resolves before AutomaticEnv)", () => { + it("an explicit-but-empty --network-id= skips the env var entirely (an explicit flag beats the env var)", () => { process.env[KEY] = "env-network"; expect(resolve("", { [KEY]: "project-network" })).toBe(localNetworkId("my-app")); }); diff --git a/apps/cli/src/command-internal/experimental-gate.unit.test.ts b/apps/cli/src/command-internal/experimental-gate.unit.test.ts index c0cdc0f60d..a068dd15d5 100644 --- a/apps/cli/src/command-internal/experimental-gate.unit.test.ts +++ b/apps/cli/src/command-internal/experimental-gate.unit.test.ts @@ -26,7 +26,7 @@ describe("requireExperimental", () => { }), ); - it.effect("passes when SUPABASE_EXPERIMENTAL=1 even without the flag (viper AutomaticEnv)", () => + it.effect("passes when SUPABASE_EXPERIMENTAL=1 even without the flag", () => Effect.gen(function* () { const saved = process.env[ENV]; process.env[ENV] = "1"; @@ -37,20 +37,18 @@ describe("requireExperimental", () => { }), ); - it.effect( - "fails even with SUPABASE_EXPERIMENTAL=1 when --experimental=false is explicit (viper Changed wins)", - () => - Effect.gen(function* () { - const saved = process.env[ENV]; - process.env[ENV] = "1"; - const error = yield* requireExperimental.pipe( - Effect.provide(withFlag(false, ["--experimental=false"])), - Effect.flip, - ); - if (saved === undefined) delete process.env[ENV]; - else process.env[ENV] = saved; - expect(error).toBeInstanceOf(ExperimentalRequiredError); - }), + it.effect("fails even with SUPABASE_EXPERIMENTAL=1 when --experimental=false is explicit", () => + Effect.gen(function* () { + const saved = process.env[ENV]; + process.env[ENV] = "1"; + const error = yield* requireExperimental.pipe( + Effect.provide(withFlag(false, ["--experimental=false"])), + Effect.flip, + ); + if (saved === undefined) delete process.env[ENV]; + else process.env[ENV] = saved; + expect(error).toBeInstanceOf(ExperimentalRequiredError); + }), ); it.effect( @@ -69,32 +67,28 @@ describe("requireExperimental", () => { }), ); - it.effect( - "a repeated --experimental=false --experimental=true keeps the LAST occurrence (viper Set() wins)", - () => - Effect.gen(function* () { - const exit = yield* requireExperimental.pipe( - Effect.provide( - withFlag(false, ["db", "pull", "--experimental=false", "--experimental=true"]), - ), - Effect.exit, - ); - expect(exit._tag).toBe("Success"); - }), + it.effect("a repeated --experimental=false --experimental=true keeps the LAST occurrence", () => + Effect.gen(function* () { + const exit = yield* requireExperimental.pipe( + Effect.provide( + withFlag(false, ["db", "pull", "--experimental=false", "--experimental=true"]), + ), + Effect.exit, + ); + expect(exit._tag).toBe("Success"); + }), ); - it.effect( - "a repeated --experimental=true --experimental=false keeps the LAST occurrence (viper Set() wins)", - () => - Effect.gen(function* () { - const error = yield* requireExperimental.pipe( - Effect.provide( - withFlag(true, ["db", "pull", "--experimental=true", "--experimental=false"]), - ), - Effect.flip, - ); - expect(error).toBeInstanceOf(ExperimentalRequiredError); - }), + it.effect("a repeated --experimental=true --experimental=false keeps the LAST occurrence", () => + Effect.gen(function* () { + const error = yield* requireExperimental.pipe( + Effect.provide( + withFlag(true, ["db", "pull", "--experimental=true", "--experimental=false"]), + ), + Effect.flip, + ); + expect(error).toBeInstanceOf(ExperimentalRequiredError); + }), ); it.effect("a repeated --experimental=false --experimental (bare) keeps the LAST occurrence", () => diff --git a/apps/cli/src/command-internal/go-output.encoders.unit.test.ts b/apps/cli/src/command-internal/go-output.encoders.unit.test.ts index 98738ed362..c9b17e719b 100644 --- a/apps/cli/src/command-internal/go-output.encoders.unit.test.ts +++ b/apps/cli/src/command-internal/go-output.encoders.unit.test.ts @@ -152,7 +152,7 @@ describe("encodeEnv", () => { expect(lines).toContain("PHYSICAL_BACKUP_DATA_LATEST_PHYSICAL_BACKUP_DATE_UNIX=1700001000"); }); - it("collapses arrays to a single empty leaf (Go viper does not descend into slices)", () => { + it("collapses arrays to a single empty leaf", () => { const out = encodeEnv(SAMPLE_RESPONSE); const lines = out.split("\n"); expect(lines).toContain('BACKUPS=""'); @@ -187,7 +187,7 @@ describe("encodeEnv", () => { expect(out.split("\n")).toEqual(["A=2", "M=3", "Z=1"]); }); - it("omits empty nested maps entirely (Go viper parity)", () => { + it("omits empty nested maps entirely", () => { expect(encodeEnv({ physical_backup_data: {} })).toBe(""); }); diff --git a/apps/cli/src/command-internal/local-config-values.unit.test.ts b/apps/cli/src/command-internal/local-config-values.unit.test.ts index b0d58ddb37..052f0a5ad3 100644 --- a/apps/cli/src/command-internal/local-config-values.unit.test.ts +++ b/apps/cli/src/command-internal/local-config-values.unit.test.ts @@ -317,7 +317,7 @@ describe("resolveLocalConfigValues", () => { expect(values.serviceRoleKey).toBe("env-service-role"); }); - it("treats an empty env var as unset, matching Viper's default", () => { + it("treats an empty env var as unset", () => { process.env["SUPABASE_AUTH_JWT_SECRET"] = ""; const config = baseConfig({ auth: { jwt_secret: "a".repeat(32) } }); const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); @@ -457,7 +457,7 @@ describe("resolveLocalConfigValues", () => { expect(values.apiUrl).toBe("https://env-override.example"); }); - it("treats an empty non-auth env var as unset, matching Viper's default", () => { + it("treats an empty non-auth env var as unset", () => { process.env["SUPABASE_DB_PORT"] = ""; const config = baseConfig({ db: { port: 54322 } }); const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); @@ -646,7 +646,7 @@ describe("resolveLocalConfigValues", () => { ); }); - it("treats an empty SUPABASE_DB_MAJOR_VERSION override as unset, matching Viper's default", () => { + it("treats an empty SUPABASE_DB_MAJOR_VERSION override as unset", () => { process.env["SUPABASE_DB_MAJOR_VERSION"] = ""; const config = baseConfig({ db: { major_version: 17 } }); expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); @@ -738,7 +738,7 @@ describe("resolveLocalConfigValues", () => { ); }); - it("treats an empty SUPABASE_EDGE_RUNTIME_DENO_VERSION override as unset, matching Viper's default", () => { + it("treats an empty SUPABASE_EDGE_RUNTIME_DENO_VERSION override as unset", () => { process.env["SUPABASE_EDGE_RUNTIME_DENO_VERSION"] = ""; const config = baseConfig({ edge_runtime: { deno_version: 2 } }); expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); @@ -898,7 +898,7 @@ describe("resolveLocalConfigValues", () => { ); }); - it("treats an empty override as unset, matching Viper's default", () => { + it("treats an empty override as unset", () => { process.env["SUPABASE_API_TLS_ENABLED"] = ""; const config = baseConfig({ api: { tls: { enabled: true }, port: 54321 } }); const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); diff --git a/apps/cli/src/command-internal/migration-apply.unit.test.ts b/apps/cli/src/command-internal/migration-apply.unit.test.ts index e1b33615b0..cb9326b733 100644 --- a/apps/cli/src/command-internal/migration-apply.unit.test.ts +++ b/apps/cli/src/command-internal/migration-apply.unit.test.ts @@ -1309,7 +1309,7 @@ describe("applySchemaFiles", () => { ); it.effect( - "falls back to Go's hardcoded default cap when SUPABASE_SCANNER_BUFFER_SIZE is set but unparseable (viper parity, not '5M' == 5MiB)", + "falls back to Go's hardcoded default cap when SUPABASE_SCANNER_BUFFER_SIZE is set but unparseable (not '5M' == 5MiB)", () => { const dir = mkdtempSync(join(tmpdir(), "schema-files-scanner-garbage-")); mkdirSync(join(dir, "supabase"), { recursive: true }); diff --git a/apps/cli/src/command-internal/pflag-reconcile.unit.test.ts b/apps/cli/src/command-internal/pflag-reconcile.unit.test.ts index 8b3e8f7d88..2d4d010336 100644 --- a/apps/cli/src/command-internal/pflag-reconcile.unit.test.ts +++ b/apps/cli/src/command-internal/pflag-reconcile.unit.test.ts @@ -206,7 +206,7 @@ describe("pflagWorkdirValue", () => { expect(pflagWorkdirValue(scan([]), Option.none(), "/env")).toEqual(Option.some("/env")); }); - it("treats a changed-but-empty flag as the walk-up default, shadowing the env var (viper precedence)", () => { + it("treats a changed-but-empty flag as the walk-up default, shadowing the env var", () => { // --workdir= expect(pflagWorkdirValue(scan([["workdir", [""]]]), Option.none(), "/env")).toEqual( Option.none(), diff --git a/apps/cli/src/command-internal/profile-load.unit.test.ts b/apps/cli/src/command-internal/profile-load.unit.test.ts index d4978e6521..0e113a740e 100644 --- a/apps/cli/src/command-internal/profile-load.unit.test.ts +++ b/apps/cli/src/command-internal/profile-load.unit.test.ts @@ -39,7 +39,7 @@ describe("loadProfile", () => { }), ); - it.effect("fails on an empty token with viper's search-mode error (Go `--profile=`)", () => + it.effect("fails an empty --profile= token with the search-mode error", () => Effect.gen(function* () { expect(yield* loadError("")).toBe( `failed to read profile: Config File "config" Not Found in "[]"`, @@ -98,7 +98,7 @@ describe("loadProfile", () => { }), ); - it.effect("accepts mixed-case keys like viper's insensitive decode (probed on go1.26)", () => + it.effect("accepts mixed-case keys case-insensitively", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const file = writeProfile( @@ -141,7 +141,7 @@ describe("loadProfile", () => { }).pipe(Effect.provide(BunServices.layer)), ); - it.effect("reports unknown keys LOWERCASED, like viper's pre-decode normalization", () => + it.effect("reports unknown keys LOWERCASED", () => Effect.gen(function* () { const file = writeProfile( "bogus-upper.yml", @@ -228,23 +228,21 @@ describe("loadProfile", () => { }), ); - it.effect( - "weakly stringifies scalars like viper, so `api_url: 123` fails http_url, not decoding", - () => - Effect.gen(function* () { - const file = writeProfile( - "typebad.yml", - [ - "name: t", - "api_url: 123", - "dashboard_url: http://127.0.0.1:44444/dashboard", - "project_host: supabase.co", - ].join("\n"), - ); - expect(yield* loadError(file)).toBe( - "invalid profile: Key: 'Profile.APIURL' Error:Field validation for 'APIURL' failed on the 'http_url' tag", - ); - }), + it.effect("weakly stringifies scalars, so `api_url: 123` fails http_url, not decoding", () => + Effect.gen(function* () { + const file = writeProfile( + "typebad.yml", + [ + "name: t", + "api_url: 123", + "dashboard_url: http://127.0.0.1:44444/dashboard", + "project_host: supabase.co", + ].join("\n"), + ); + expect(yield* loadError(file)).toBe( + "invalid profile: Key: 'Profile.APIURL' Error:Field validation for 'APIURL' failed on the 'http_url' tag", + ); + }), ); it.effect("validates the hostname_rfc1123 and http_url format tags", () => @@ -267,7 +265,7 @@ describe("loadProfile", () => { }), ); - it.effect("fails a malformed YAML file closed with viper's parse prefix", () => + it.effect("fails a malformed YAML file closed with the parse-error prefix", () => Effect.gen(function* () { const file = writeProfile("malformed.yml", "name: [broken\n api_url"); const message = yield* loadError(file); diff --git a/apps/cli/src/command-internal/upgrade-notice.unit.test.ts b/apps/cli/src/command-internal/upgrade-notice.unit.test.ts index 26d7067b1a..edec5d7c7d 100644 --- a/apps/cli/src/command-internal/upgrade-notice.unit.test.ts +++ b/apps/cli/src/command-internal/upgrade-notice.unit.test.ts @@ -205,7 +205,7 @@ describe("runUpgradeNotice", () => { ctx.cleanup(); }); - it("SUPABASE_DEBUG surfaces fetch failures without the --debug flag, like viper's AutomaticEnv", async () => { + it("SUPABASE_DEBUG surfaces fetch failures without the --debug flag", async () => { const ctx = setup({ fetchFails: true, project: false, env: { SUPABASE_DEBUG: "1" } }); await runUpgradeNotice(ctx.deps); expect(ctx.stderr).toContain("Failed to fetch latest release"); @@ -444,7 +444,7 @@ describe("runUpgradeNotice", () => { ctx.cleanup(); }); - it("an explicit empty --workdir beats SUPABASE_WORKDIR and falls back to the walk, like viper", async () => { + it("an explicit empty --workdir beats SUPABASE_WORKDIR and falls back to the walk", async () => { const ctx = setup({}); const envDir = join(workdir, "env-project"); mkdirSync(join(envDir, "supabase"), { recursive: true }); diff --git a/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md b/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md index e258425a89..d0e03d5dec 100644 --- a/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md @@ -240,7 +240,7 @@ publishes only when the roles file is unchanged, and retention keeps three entri A falsy `SUPABASE_SHADOW_CACHE` bypasses this cache. A failed warm restore recreates the database; a failed cache export warns and continues with the live shadow. -On by default; setting `SUPABASE_SHADOW_CACHE` to anything not viper-true (`false`/`0`/empty/garbage, +On by default; setting `SUPABASE_SHADOW_CACHE` to anything not boolean-true (`false`/`0`/empty/garbage, honored from the ambient env AND the project's dotenv, e.g. `supabase/.env`) turns it off, restoring the documented uncached lifecycle. A warm hit skips the platform baseline, so the `Initialising schema...` progress line does not print — diff --git a/apps/cli/src/commands/db/dump/SIDE_EFFECTS.md b/apps/cli/src/commands/db/dump/SIDE_EFFECTS.md index ba27b252c2..bc3a62f524 100644 --- a/apps/cli/src/commands/db/dump/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/dump/SIDE_EFFECTS.md @@ -33,15 +33,15 @@ image), to stdout or `--file`. ## Environment Variables -| Variable | Purpose | -| ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `SUPABASE_DB_PASSWORD` (`DB_PASSWORD` viper key; `--password`/`-p` overrides) | remote DB password | -| `SUPABASE_ACCESS_TOKEN` | `--linked` auth | -| `BITBUCKET_CLONE_DIR` | (no-op for dump — no `--security-opt` is set) | -| `SUPABASE_INTERNAL_IMAGE_REGISTRY` | rewrite the pg image registry for compose dumps; stack dumps use the catalog image pin unchanged | -| `SUPABASE_USE_SLIM_IMAGES` | resolve the current Postgres pin from the slim `ghcr.io/supabase/cli` builds (`true`/`1` enable); majors 13/15 use `15.14.1.167` when the flag is on; historical pins, PG14, OrioleDB, and flag-off `15.8.1.085` stay on docker.io | -| `DOCKER_HOST` | docker daemon endpoint | -| `MSYSTEM`, `TERM_PROGRAM` | suppress the piped-stdout non-ASCII warning in MSYS/mintty sessions | +| Variable | Purpose | +| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `SUPABASE_DB_PASSWORD` (`--password`/`-p` overrides) | remote DB password | +| `SUPABASE_ACCESS_TOKEN` | `--linked` auth | +| `BITBUCKET_CLONE_DIR` | (no-op for dump — no `--security-opt` is set) | +| `SUPABASE_INTERNAL_IMAGE_REGISTRY` | rewrite the pg image registry for compose dumps; stack dumps use the catalog image pin unchanged | +| `SUPABASE_USE_SLIM_IMAGES` | resolve the current Postgres pin from the slim `ghcr.io/supabase/cli` builds (`true`/`1` enable); majors 13/15 use `15.14.1.167` when the flag is on; historical pins, PG14, OrioleDB, and flag-off `15.8.1.085` stay on docker.io | +| `DOCKER_HOST` | docker daemon endpoint | +| `MSYSTEM`, `TERM_PROGRAM` | suppress the piped-stdout non-ASCII warning in MSYS/mintty sessions | ## Exit Codes diff --git a/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md b/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md index d6351a390a..21aefce12a 100644 --- a/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md @@ -91,7 +91,7 @@ publishes only when the roles file is unchanged, and retention keeps three entri A falsy `SUPABASE_SHADOW_CACHE` bypasses this cache. A failed warm restore recreates the database; a failed cache export warns and continues with the live shadow. -On by default; setting `SUPABASE_SHADOW_CACHE` to anything not viper-true (`false`/`0`/empty/garbage, +On by default; setting `SUPABASE_SHADOW_CACHE` to anything not boolean-true (`false`/`0`/empty/garbage, honored from the ambient env AND the project's dotenv, e.g. `supabase/.env`) turns it off, restoring the documented uncached lifecycle. A warm hit skips the platform baseline, so the `Initialising schema...` progress line does not print — diff --git a/apps/cli/src/commands/db/pull/pull.integration.test.ts b/apps/cli/src/commands/db/pull/pull.integration.test.ts index 6b3e31dc65..84a3a49901 100644 --- a/apps/cli/src/commands/db/pull/pull.integration.test.ts +++ b/apps/cli/src/commands/db/pull/pull.integration.test.ts @@ -1893,7 +1893,7 @@ describe("db pull", () => { it.effect( "a repeated --experimental=false --experimental=true still exports (last Set() wins)", () => { - // pflag/viper bind one variable per flag: repeated occurrences collapse to + // Each flag binds one variable: repeated occurrences collapse to // whichever Set() call happened last, so a resolver must not get this ordering // backwards. const s = setup(tmp.current, { diff --git a/apps/cli/src/commands/db/reset/SIDE_EFFECTS.md b/apps/cli/src/commands/db/reset/SIDE_EFFECTS.md index 62b73f9b3f..48a128a82b 100644 --- a/apps/cli/src/commands/db/reset/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/reset/SIDE_EFFECTS.md @@ -169,7 +169,7 @@ the whole reset** (not just "skip buckets"). | `SUPABASE_YES` | auto-confirm the reset prompt and the local path's bucket-seed overwrite/prune prompts (shell or project dotenv, same as `seed buckets`) | no (also `--yes`) | | `SUPABASE_EXPERIMENTAL` | selects the schema-files apply branch on either target | no (also `--experimental`) | | `SUPABASE_EXPERIMENTAL_PGDELTA_ENABLED` | overrides `[experimental.pgdelta].enabled`; a truthy value flips the reset gate (`experimental && resolvedVersion === "" && !toml.pgDelta.enabled`) back to timestamped migrations even with `--experimental` set — switches between two different destructive code paths | no | -| `SUPABASE_DB_MIGRATIONS_SCHEMA_PATHS` | overrides `[db.migrations].schema_paths` (viper `AutomaticEnv`, beats the config-file value) for the schema-files apply branch — genuinely effective on both targets now | no (no dedicated flag — config-file-only otherwise) | +| `SUPABASE_DB_MIGRATIONS_SCHEMA_PATHS` | overrides `[db.migrations].schema_paths` (beats the config-file value) for the schema-files apply branch — genuinely effective on both targets now | no (no dedicated flag — config-file-only otherwise) | | `SUPABASE_PROJECT_ID` | overrides the local container id; ALSO the linked-ref resolution fallback `--project-ref` supersedes — see Notes for the narrower scope of the flag | no | | `SUPABASE_INTERNAL_IMAGE_REGISTRY` | overrides the image registry used to resolve the local path's container images (project `.env` or shell) | no (project `.env` or shell) | | `SUPABASE_USE_SLIM_IMAGES` | resolves the local-reset Postgres image and the realtime/storage/auth migrate-job images from slim `ghcr.io/supabase/cli` builds (`true`/`1` enable); majors 13/15 use `15.14.1.167` when the flag is on; historical pins, PG14, OrioleDB, and flag-off `15.8.1.085` stay on docker.io | no (ambient shell only) | diff --git a/apps/cli/src/commands/db/schema/declarative/sync/SIDE_EFFECTS.md b/apps/cli/src/commands/db/schema/declarative/sync/SIDE_EFFECTS.md index d9fb9bdfd1..d4f2fa008d 100644 --- a/apps/cli/src/commands/db/schema/declarative/sync/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/schema/declarative/sync/SIDE_EFFECTS.md @@ -154,7 +154,7 @@ restore recreates the database; failed publication warns and keeps the live shad The bundled (pg-delta next) engine provisions both plan shadows through `acquireShadowDatabase` (`pgdelta-next-shadow.layer.ts`): on by default, off when -`SUPABASE_SHADOW_CACHE` is set to anything not viper-true (ambient env or project dotenv); `--no-cache` +`SUPABASE_SHADOW_CACHE` is set to anything not boolean-true (ambient env or project dotenv); `--no-cache` bypasses restore and publish for that invocation. Next allocates an ephemeral host port per shadow; the cache key hashes the cluster recipe (including the effective Webhooks/`pg_net` policy), not the published port, so worktrees and repeated syncs with the same settings share diff --git a/apps/cli/src/commands/network-restrictions/get/SIDE_EFFECTS.md b/apps/cli/src/commands/network-restrictions/get/SIDE_EFFECTS.md index c671ea8eda..2081c28045 100644 --- a/apps/cli/src/commands/network-restrictions/get/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/network-restrictions/get/SIDE_EFFECTS.md @@ -73,7 +73,7 @@ CLI-1975 struct-spec ones): - `json` — alphabetical struct-field order with trailing newline. - `yaml` — `stringifyYaml(response)`. - `toml` — `stringifyToml(response)` with trailing newline. -- `env` — Viper-flattened SCREAMING_SNAKE_CASE keys. +- `env` — flattened SCREAMING_SNAKE_CASE keys. ### `--output pretty` diff --git a/apps/cli/src/commands/start/SIDE_EFFECTS.md b/apps/cli/src/commands/start/SIDE_EFFECTS.md index c95688c094..58cc6fce47 100644 --- a/apps/cli/src/commands/start/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/start/SIDE_EFFECTS.md @@ -195,7 +195,7 @@ not implemented. | Variable | Purpose | Required? | | -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -| `SUPABASE_*` (any dotted config field) | Generic Viper-style `AutomaticEnv` override of any `config.toml` field (e.g. `SUPABASE_AUTH_ENABLED`, `SUPABASE_API_PORT`) | no | +| `SUPABASE_*` (any dotted config field) | Generic env override of any `config.toml` field (e.g. `SUPABASE_AUTH_ENABLED`, `SUPABASE_API_PORT`) | no | | `SUPABASE_EXPERIMENTAL` (or `--experimental`) | Fresh volume + no pg-delta: applies `db.migrations.schema_paths` files instead of `migrations/*.sql` (see "Fresh-volume DB setup" above) | no | | `SUPABASE_INTERNAL_IMAGE_REGISTRY` | Overrides the image registry used to resolve every service's image | no | | `SUPABASE_PROJECT_ID` | Overrides the resolved local project id (env → config.toml → workdir basename) | no | diff --git a/apps/cli/src/commands/status/SIDE_EFFECTS.md b/apps/cli/src/commands/status/SIDE_EFFECTS.md index 602836ae79..1a7b8bd35c 100644 --- a/apps/cli/src/commands/status/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/status/SIDE_EFFECTS.md @@ -391,7 +391,7 @@ the Go CLI's own contract exactly. The additive failure envelope above is scoped `SUPABASE_AUTH_ANON_KEY`/`SUPABASE_AUTH_SERVICE_ROLE_KEY` override the corresponding `config.toml` value at higher precedence — an empty env var is treated as unset. This is scoped to exactly the 5 auth fields `status` reads; it is not a - general `@supabase/config` port of Viper's `AutomaticEnv` (which applies to every config field). + general `SUPABASE_*` override of every config field. - `db.password` and the `storage.s3_credentials` triple have no `@supabase/config` schema field; the old Go CLI hardcoded both (`"postgres"` and the S3 access key/secret/region seen above), reproduced identically in `local-config-values.ts`. diff --git a/apps/cli/src/shared/cli/cobra-flag-groups.ts b/apps/cli/src/shared/cli/cobra-flag-groups.ts index 9380bf88d2..2f84881bcc 100644 --- a/apps/cli/src/shared/cli/cobra-flag-groups.ts +++ b/apps/cli/src/shared/cli/cobra-flag-groups.ts @@ -38,7 +38,7 @@ const PFLAG_BOOLEAN_FALSE_VALUES: ReadonlySet = new Set([ /** * Last explicit `--`/`--=` boolean occurrence in argv (`undefined` if - * absent), matching pflag/viper's last-`Set()`-wins semantics. A bare flag is true; an inline + * absent), last occurrence wins. A bare flag is true; an inline * value is false only when it matches pflag's boolean false set, otherwise truthy. */ export function explicitBooleanLongFlag( From bb04a84cc0ad08094622f073a063856aeb4644cc Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Thu, 8 Oct 2026 14:24:36 +0100 Subject: [PATCH 4/9] docs(cli): describe current behaviour without Go CLI framing SIDE_EFFECTS.md files, CLI docs, AGENTS.md, and the config README now state behaviour directly instead of comparing against the removed Go CLI, pflag, viper, or cobra. ADRs are left as historical records. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- apps/cli/docs/binary-distribution.md | 10 -- apps/cli/docs/profile.md | 10 +- apps/cli/docs/supabase/login.md | 2 +- apps/cli/src/SIDE_EFFECTS_TEMPLATE.md | 2 +- .../src/commands/backups/list/SIDE_EFFECTS.md | 2 +- .../src/commands/bootstrap/SIDE_EFFECTS.md | 18 +-- .../commands/branches/create/SIDE_EFFECTS.md | 18 +-- .../commands/branches/delete/SIDE_EFFECTS.md | 2 +- .../commands/branches/disable/SIDE_EFFECTS.md | 2 +- .../src/commands/branches/get/SIDE_EFFECTS.md | 4 +- .../commands/branches/list/SIDE_EFFECTS.md | 24 ++-- .../commands/branches/pause/SIDE_EFFECTS.md | 2 +- .../commands/branches/unpause/SIDE_EFFECTS.md | 2 +- .../commands/branches/update/SIDE_EFFECTS.md | 4 +- .../src/commands/completion/SIDE_EFFECTS.md | 70 +++++------ .../src/commands/config/diff/SIDE_EFFECTS.md | 5 +- .../src/commands/config/pull/SIDE_EFFECTS.md | 5 +- .../src/commands/db/advisors/SIDE_EFFECTS.md | 6 +- apps/cli/src/commands/db/diff/SIDE_EFFECTS.md | 26 ++-- apps/cli/src/commands/db/dump/SIDE_EFFECTS.md | 12 +- apps/cli/src/commands/db/lint/SIDE_EFFECTS.md | 6 +- apps/cli/src/commands/db/pull/SIDE_EFFECTS.md | 20 ++-- apps/cli/src/commands/db/push/SIDE_EFFECTS.md | 3 +- .../cli/src/commands/db/query/SIDE_EFFECTS.md | 13 +- .../commands/db/remote/commit/SIDE_EFFECTS.md | 11 +- .../cli/src/commands/db/reset/SIDE_EFFECTS.md | 40 +++---- .../cli/src/commands/db/start/SIDE_EFFECTS.md | 10 +- apps/cli/src/commands/db/test/SIDE_EFFECTS.md | 22 ++-- apps/cli/src/commands/domains/SIDE_EFFECTS.md | 20 ++-- .../src/commands/encryption/SIDE_EFFECTS.md | 16 ++- .../commands/functions/deploy/SIDE_EFFECTS.md | 14 +-- .../functions/download/SIDE_EFFECTS.md | 4 +- .../commands/functions/list/SIDE_EFFECTS.md | 4 +- .../commands/functions/serve/SIDE_EFFECTS.md | 2 +- .../commands/gen/bearer-jwt/SIDE_EFFECTS.md | 9 +- .../src/commands/gen/types/SIDE_EFFECTS.md | 16 +-- .../src/commands/inspect/db/SIDE_EFFECTS.md | 20 ++-- .../commands/inspect/report/SIDE_EFFECTS.md | 31 ++--- apps/cli/src/commands/link/SIDE_EFFECTS.md | 113 ++++++++---------- apps/cli/src/commands/login/SIDE_EFFECTS.md | 2 +- apps/cli/src/commands/logout/SIDE_EFFECTS.md | 2 +- .../commands/migration/down/SIDE_EFFECTS.md | 8 +- .../commands/migration/fetch/SIDE_EFFECTS.md | 7 +- .../commands/migration/list/SIDE_EFFECTS.md | 8 +- .../commands/migration/new/SIDE_EFFECTS.md | 2 +- .../commands/migration/repair/SIDE_EFFECTS.md | 17 ++- .../commands/migration/squash/SIDE_EFFECTS.md | 26 ++-- .../src/commands/migration/up/SIDE_EFFECTS.md | 5 +- .../src/commands/orgs/create/SIDE_EFFECTS.md | 9 +- .../src/commands/orgs/list/SIDE_EFFECTS.md | 8 +- .../postgres-config/delete/SIDE_EFFECTS.md | 4 +- .../postgres-config/get/SIDE_EFFECTS.md | 2 +- .../postgres-config/update/SIDE_EFFECTS.md | 4 +- .../cli/src/commands/projects/SIDE_EFFECTS.md | 12 +- .../projects/api-keys/SIDE_EFFECTS.md | 4 +- .../commands/projects/create/SIDE_EFFECTS.md | 29 ++--- .../commands/projects/delete/SIDE_EFFECTS.md | 2 +- .../commands/projects/list/SIDE_EFFECTS.md | 16 +-- apps/cli/src/commands/pull/SIDE_EFFECTS.md | 4 +- .../src/commands/secrets/list/SIDE_EFFECTS.md | 2 +- .../src/commands/seed/buckets/SIDE_EFFECTS.md | 7 +- .../cli/src/commands/services/SIDE_EFFECTS.md | 14 +-- .../snippets/download/SIDE_EFFECTS.md | 2 +- .../commands/snippets/list/SIDE_EFFECTS.md | 4 +- apps/cli/src/commands/sso/add/SIDE_EFFECTS.md | 48 ++++---- .../cli/src/commands/sso/info/SIDE_EFFECTS.md | 4 +- .../cli/src/commands/sso/list/SIDE_EFFECTS.md | 4 +- .../src/commands/sso/remove/SIDE_EFFECTS.md | 4 +- .../cli/src/commands/sso/show/SIDE_EFFECTS.md | 4 +- .../src/commands/sso/update/SIDE_EFFECTS.md | 48 ++++---- apps/cli/src/commands/start/SIDE_EFFECTS.md | 69 +++++------ apps/cli/src/commands/status/SIDE_EFFECTS.md | 70 +++++------ apps/cli/src/commands/stop/SIDE_EFFECTS.md | 25 ++-- .../src/commands/storage/cp/SIDE_EFFECTS.md | 7 +- .../src/commands/storage/ls/SIDE_EFFECTS.md | 2 +- .../src/commands/storage/mv/SIDE_EFFECTS.md | 2 +- .../src/commands/storage/rm/SIDE_EFFECTS.md | 2 +- apps/cli/src/commands/test/db/SIDE_EFFECTS.md | 22 ++-- .../cli/src/commands/test/new/SIDE_EFFECTS.md | 6 +- apps/cli/src/commands/unlink/SIDE_EFFECTS.md | 7 +- docs/cli/dev-alpha-command-structure.md | 4 +- packages/config/README.md | 3 +- packages/config/docs/cli-config-loading.md | 2 +- 84 files changed, 497 insertions(+), 638 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index eb5e0c2e11..dd495e39ac 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -111,7 +111,7 @@ page, or upstream issue. If the rationale needs more than three lines, move it t ### Never write - Code narration, provenance/history, evidence trails, ticket IDs as provenance, or meta-commentary on code shape. A ticket ID belongs only in a `TODO(CLI-1234):` or when no ADR exists and the ticket is the only home for a decision. -- Restatements of docs, section banners, or Go-parity framing. Link to maintained docs instead. +- Restatements of docs, or section banners. Link to maintained docs instead. - ALL-CAPS emphasis or words such as “deliberately”, “crucially”, and “exactly”. Tests should carry intent in their names; comments only explain non-obvious fixture setup. Keep diff --git a/apps/cli/docs/binary-distribution.md b/apps/cli/docs/binary-distribution.md index cf111b3bb5..1c9705a51d 100644 --- a/apps/cli/docs/binary-distribution.md +++ b/apps/cli/docs/binary-distribution.md @@ -59,17 +59,7 @@ This: 3. Bundles the binary into the platform archives (`.tar.gz` / `.zip`) 4. Includes the binary in the Linux package manager packages (deb/rpm/apk) -## History - -The CLI was originally a two-binary distribution during its gradual port from a Go CLI to -TypeScript: a `supabase` Bun single-file executable alongside a `supabase-go` binary that the TS -CLI proxied to for not-yet-ported commands. The port completed (CLI-1970), the last residual -delegation surface was removed (CLI-2432), and the Go source tree (`apps/cli-go/`) was deleted in -the same effort — the CLI now ships as a single compiled binary per platform. See -[ADR 0030](../../../docs/adr/0030-go-cli-removal.md) for the full removal record. - ## See Also - [ADR 0011](../../../docs/adr/0011-cli-release-and-distribution-strategy.md) — the release & distribution strategy decision (binary packaging choice, per-channel publish mechanisms, CI pipeline design, open blockers). -- [ADR 0030](../../../docs/adr/0030-go-cli-removal.md) — the Go CLI removal decision record. - [`release-process.md`](./release-process.md) — operational playbook for local, PoC, and production releases. diff --git a/apps/cli/docs/profile.md b/apps/cli/docs/profile.md index 59949999a6..b5b6065a67 100644 --- a/apps/cli/docs/profile.md +++ b/apps/cli/docs/profile.md @@ -4,8 +4,8 @@ This document captures the intended design for advanced non-default backend rout CLI. The file is intentionally named `profile.md` to preserve continuity with the earlier discussion -and the old Go CLI concept. The design in this document does **not** recommend carrying the old -`profile` abstraction forward. The preferred term in the new CLI is **platform**. +and the previous `profile` concept. The design in this document does **not** recommend carrying the +previous `profile` abstraction forward. The preferred term in the new CLI is **platform**. ## Overview @@ -21,9 +21,9 @@ A much smaller group of users needs to point the CLI somewhere else: The goal is to support those cases cleanly without turning non-default backend routing into a first-class everyday feature. -## Why Not Go-Style Profiles +## Why Not Profiles -The old Go CLI `profile` abstraction became too broad. +The previous `profile` abstraction became too broad. It started as a way to switch API endpoints, but it ended up bundling multiple unrelated concerns: @@ -195,7 +195,7 @@ This design does not propose: - a built-in first-party staging alias in the public UX - customer-specific platform definitions baked into the CLI - plugin or product behavior encoded directly inside platform config -- backward compatibility with Go CLI `profile` names or storage behavior +- backward compatibility with previous `profile` names or storage behavior ## Open Follow-up Work diff --git a/apps/cli/docs/supabase/login.md b/apps/cli/docs/supabase/login.md index 0387dbd3f6..5dbc7df448 100644 --- a/apps/cli/docs/supabase/login.md +++ b/apps/cli/docs/supabase/login.md @@ -2,7 +2,7 @@ Connect the Supabase CLI to your Supabase account by logging in with your [personal access token](https://supabase.com/dashboard/account/tokens). -Your access token is stored securely in [native credentials storage](https://github.com/zalando/go-keyring#dependencies). If native credentials storage is unavailable, it will be written to a plain text file at `/access-token`. +Your access token is stored securely in native credentials storage (the operating system keyring). If native credentials storage is unavailable, it will be written to a plain text file at `/access-token`. > If this behavior is not desired, such as in a CI environment, you may skip login by specifying the `SUPABASE_ACCESS_TOKEN` environment variable in other commands. diff --git a/apps/cli/src/SIDE_EFFECTS_TEMPLATE.md b/apps/cli/src/SIDE_EFFECTS_TEMPLATE.md index cfb21c9fa4..b2b4bb1a51 100644 --- a/apps/cli/src/SIDE_EFFECTS_TEMPLATE.md +++ b/apps/cli/src/SIDE_EFFECTS_TEMPLATE.md @@ -120,5 +120,5 @@ diff --git a/apps/cli/src/commands/backups/list/SIDE_EFFECTS.md b/apps/cli/src/commands/backups/list/SIDE_EFFECTS.md index 1609312f06..b3689bd4b1 100644 --- a/apps/cli/src/commands/backups/list/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/backups/list/SIDE_EFFECTS.md @@ -56,7 +56,7 @@ The `--output {pretty,json,yaml,toml,env}` flag and the `--output-format {text,j ### `--output pretty` (default) / `--output-format text` -For PITR-only projects, prints a Glamour-styled markdown table with columns: `REGION`, `WALG`, `PITR`, `EARLIEST TIMESTAMP`, `LATEST TIMESTAMP`. For projects with logical/physical backups, prints columns: `REGION`, `BACKUP TYPE`, `STATUS`, `CREATED AT (UTC)`. The table is rendered byte-for-byte using `glamour.WithStandardStyle(styles.AsciiStyle)`. +For PITR-only projects, prints a styled markdown table with columns: `REGION`, `WALG`, `PITR`, `EARLIEST TIMESTAMP`, `LATEST TIMESTAMP`. For projects with logical/physical backups, prints columns: `REGION`, `BACKUP TYPE`, `STATUS`, `CREATED AT (UTC)`. ### `--output json` diff --git a/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md b/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md index 26e46f18c5..5099d23869 100644 --- a/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md @@ -23,15 +23,15 @@ command — see Notes). ## Files Written -| Path | Format | When | -| ----------------------------------------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `/supabase/config.toml` | TOML | blank/`scratch` path only (via `initProject`) | -| `/