From b22d958fdb418b7a1f07a1b1d71259ebf5ae7e88 Mon Sep 17 00:00:00 2001 From: avallete Date: Thu, 17 Sep 2026 16:58:37 +0200 Subject: [PATCH 1/4] feat(cli): opt supabase init into the experimental stack via env SUPABASE_EXPERIMENTAL_STACK=1 writes experimental.stack and omits Docker-era default ports so new projects can use the stack without pinning legacy local ports. --- apps/cli/AGENTS.md | 4 +- apps/cli/docs/stack-commands.md | 5 ++ .../src/commands/bootstrap/SIDE_EFFECTS.md | 25 +++---- .../commands/bootstrap/bootstrap.handler.ts | 6 ++ .../stack/stack-config.integration.test.ts | 8 +++ apps/cli/src/commands/init/SIDE_EFFECTS.md | 30 ++++++--- apps/cli/src/commands/init/init.handler.ts | 8 +++ .../commands/init/init.integration.test.ts | 67 +++++++++++++++++++ .../src/shared/init/project-init.templates.ts | 48 ++++++++++++- .../init/project-init.templates.unit.test.ts | 17 +++++ apps/cli/src/shared/init/project-init.ts | 4 +- 11 files changed, 198 insertions(+), 24 deletions(-) diff --git a/apps/cli/AGENTS.md b/apps/cli/AGENTS.md index 6d5f42ccbd..65f20f1b5a 100644 --- a/apps/cli/AGENTS.md +++ b/apps/cli/AGENTS.md @@ -89,7 +89,9 @@ Resolve opt-in booleans with `command-internal/experimental-feature.ts`: environ overrides the project setting, and an unset or empty value uses the config. Invalid environment values are typed failures on applicable command paths. Disabled families are absent from the command tree, help, and completion; enabled help is marked experimental and stays out of stable -generated command documentation. Environment opt-ins do not write project configuration. +generated command documentation. Environment opt-ins do not write project configuration, except +`supabase init` (and blank `bootstrap`) with `SUPABASE_EXPERIMENTAL_STACK=1`, which persists +`experimental.stack = true` and omits Docker-era default ports. Keep config-discovery failure policy explicit and cover TOML, JSON, precedence, and disabled behavior. diff --git a/apps/cli/docs/stack-commands.md b/apps/cli/docs/stack-commands.md index 46d84c73bd..2f8ecb82b0 100644 --- a/apps/cli/docs/stack-commands.md +++ b/apps/cli/docs/stack-commands.md @@ -87,6 +87,11 @@ For temporary selection, set `SUPABASE_EXPERIMENTAL_STACK=1` to select the new b precedence over `experimental.stack`; an unset or empty value falls back to the file setting. Other values are rejected. The override is applied before reading the project configuration. +`SUPABASE_EXPERIMENTAL_STACK=1 supabase init` writes `[experimental] stack = true` into the new +project config and omits the Docker-era default ports so the stack is not pinned to them. +Without the environment variable, `init` still writes the established template with those ports +and without the stack flag. Blank `supabase bootstrap` uses the same scaffold. + ## Reading stack logs `supabase stack logs` reads retained logs without starting or stopping the selected stack. Use diff --git a/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md b/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md index a7068afa9f..dd80ae60e2 100644 --- a/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md @@ -61,21 +61,22 @@ neither branch ever reaches the temp-login-role/Management-API path a passwordle ## Environment Variables -| Variable | Purpose | Required? | -| ----------------------- | ---------------------------------------------------------------------------------------------------- | --------- | -| `SUPABASE_WORKDIR` | target dir (`--workdir` flag → env → prompt → cwd) | no | -| `SUPABASE_DB_PASSWORD` | DB password (`-p` flag → env → prompt/generate) | no | -| `GITHUB_TOKEN` | raise the GitHub API rate limit for template fetch | no | -| `SUPABASE_ACCESS_TOKEN` | auth bypass for ensure-login | no | -| `SUPABASE_PROFILE` | profile name/path (env → `~/.supabase/profile` → `supabase`) | no | -| `SUPABASE_YES` | auto-confirm the native push step's prompts, read project-`.env`-aware like the standalone `db push` | no | +| Variable | Purpose | Required? | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | +| `SUPABASE_WORKDIR` | target dir (`--workdir` flag → env → prompt → cwd) | no | +| `SUPABASE_DB_PASSWORD` | DB password (`-p` flag → env → prompt/generate) | no | +| `GITHUB_TOKEN` | raise the GitHub API rate limit for template fetch | no | +| `SUPABASE_ACCESS_TOKEN` | auth bypass for ensure-login | no | +| `SUPABASE_PROFILE` | profile name/path (env → `~/.supabase/profile` → `supabase`) | no | +| `SUPABASE_YES` | auto-confirm the native push step's prompts, read project-`.env`-aware like the standalone `db push` | no | +| `SUPABASE_EXPERIMENTAL_STACK` | blank/`scratch` path only; when `1`, the `initProject` scaffold persists `[experimental] stack = true` and omits Docker-era default ports. Invalid values fail closed | no | ## Exit Codes -| Code | Condition | -| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `0` | success | -| `1` | invalid template arg; overwrite declined (`context canceled`); template list/download failure; login failure; create failure; api-keys exhausted; health unhealthy / error status; native push failure (missing local/remote migrations, cancelled confirmation, connect/apply failure); any network failure. The `.env` derive/write is **non-fatal** (prints `Failed to create .env file: ` and continues). | +| Code | Condition | +| ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `0` | success | +| `1` | invalid template arg; overwrite declined (`context canceled`); template list/download failure; login failure; create failure; api-keys exhausted; health unhealthy / error status; native push failure (missing local/remote migrations, cancelled confirmation, connect/apply failure); invalid `SUPABASE_EXPERIMENTAL_STACK` on the blank path; any network failure. The `.env` derive/write is **non-fatal** (prints `Failed to create .env file: ` and continues). | ## Telemetry diff --git a/apps/cli/src/commands/bootstrap/bootstrap.handler.ts b/apps/cli/src/commands/bootstrap/bootstrap.handler.ts index c117339785..42e7ded388 100644 --- a/apps/cli/src/commands/bootstrap/bootstrap.handler.ts +++ b/apps/cli/src/commands/bootstrap/bootstrap.handler.ts @@ -37,6 +37,7 @@ import { projectCreateCore } from "../../command-internal/project-create-core.ts import { tempPaths } from "../../command-internal/temp-paths.ts"; import { extractServiceKeys } from "../../command-internal/tenant-keys.ts"; import { parseDotEnv } from "../../command-internal/dotenv.ts"; +import { resolveExperimentalFeature } from "../../command-internal/experimental-feature.ts"; import { initProject } from "../../shared/init/project-init.ts"; import { buildDotEnv, marshalDotEnv } from "./bootstrap.dotenv.ts"; import { @@ -170,6 +171,11 @@ export const bootstrap = Effect.fn("bootstrap")(function* ( useOrioledb: false, withVscodeSettings: false, withIntellijSettings: false, + experimentalStack: yield* resolveExperimentalFeature({ + feature: "stack", + configValue: Effect.succeed(false), + env: process.env, + }), }); } diff --git a/apps/cli/src/commands/experimental/stack/stack-config.integration.test.ts b/apps/cli/src/commands/experimental/stack/stack-config.integration.test.ts index cfd51e7775..7c859d72b6 100644 --- a/apps/cli/src/commands/experimental/stack/stack-config.integration.test.ts +++ b/apps/cli/src/commands/experimental/stack/stack-config.integration.test.ts @@ -453,6 +453,14 @@ enabled = true }); }); + it.effect("leaves stack-opt-in init listeners automatic except disabled pooler", () => { + return Effect.gen(function* () { + const root = yield* project(renderCliConfigTemplate("stack-config-init", false, true)); + const config = yield* load(root); + expect(config.listeners).toEqual({ pooler: { enabled: false } }); + }); + }); + it.effect("ignores unresolved function env references when edge runtime is disabled", () => { return Effect.gen(function* () { const root = yield* project(`project_id = "stack-config-disabled-functions-env" diff --git a/apps/cli/src/commands/init/SIDE_EFFECTS.md b/apps/cli/src/commands/init/SIDE_EFFECTS.md index 0d8dc247d0..c4e9fe6bec 100644 --- a/apps/cli/src/commands/init/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/init/SIDE_EFFECTS.md @@ -12,13 +12,13 @@ ## Files Written -| Path | Format | When | -| ------------------------- | ------ | --------------------------------------------------------------------------------------------------------------- | -| `supabase/config.toml` | TOML | always on success; created from default template | -| `supabase/.gitignore` | text | when inside a git repo and the template is not already present | -| `.vscode/settings.json` | JSON | when interactive VS Code setup is accepted, or when `--with-vscode-settings` / `--with-vscode-workspace` is set | -| `.vscode/extensions.json` | JSON | when interactive VS Code setup is accepted, or when `--with-vscode-settings` / `--with-vscode-workspace` is set | -| `.idea/deno.xml` | XML | when interactive IntelliJ setup is accepted, or when `--with-intellij-settings` is set | +| Path | Format | When | +| ------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------- | +| `supabase/config.toml` | TOML | always on success; created from the default template, or the stack-opt-in template when `SUPABASE_EXPERIMENTAL_STACK=1` | +| `supabase/.gitignore` | text | when inside a git repo and the template is not already present | +| `.vscode/settings.json` | JSON | when interactive VS Code setup is accepted, or when `--with-vscode-settings` / `--with-vscode-workspace` is set | +| `.vscode/extensions.json` | JSON | when interactive VS Code setup is accepted, or when `--with-vscode-settings` / `--with-vscode-workspace` is set | +| `.idea/deno.xml` | XML | when interactive IntelliJ setup is accepted, or when `--with-intellij-settings` is set | ## API Routes @@ -28,7 +28,9 @@ ## Environment Variables -None. +| Variable | Purpose | Required? | +| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | +| `SUPABASE_EXPERIMENTAL_STACK` | when `1`, persist `[experimental] stack = true` and omit Docker-era default ports from `supabase/config.toml`; when `0` or unset, write the established template | no | ## Exit Codes @@ -36,6 +38,7 @@ None. | ---- | ------------------------------------------------------------------------------------ | | `0` | success - prints "Finished supabase init." | | `1` | `supabase/config.toml` already exists and `--force` was not provided | +| `1` | `SUPABASE_EXPERIMENTAL_STACK` is set to a value other than `0` or `1` | | `1` | permission denied writing config file | | `1` | an existing `.vscode/settings.json` / `.vscode/extensions.json` is not valid JSON(C) | @@ -76,11 +79,22 @@ required flag(s) "experimental" not set Try rerunning the command with --debug to troubleshoot the error. ``` +When `SUPABASE_EXPERIMENTAL_STACK` is set to a value other than `0` or `1` (stderr; the second line is the generic debug hint appended on error): + +``` +SUPABASE_EXPERIMENTAL_STACK must be 0 or 1 when set +Try rerunning the command with --debug to troubleshoot the error. +``` + ## Notes - Uses the invocation cwd directly and does not recurse upward looking for an existing project. - The `--force` flag overwrites an existing `supabase/config.toml`. - The `--use-orioledb` flag sets `UseOrioleDB` in init params; requires `--experimental` flag. +- `SUPABASE_EXPERIMENTAL_STACK=1` opts the new project into the experimental stack backend: the + written config includes `[experimental] stack = true` and omits the Docker-era default ports + (API, database, shadow, pooler, Studio, mail UI, Functions inspector, and Analytics). + Invalid values fail closed. `0` or unset keeps the established template. - The `--interactive` / `-i` flag enables IDE settings prompts (only effective in TTY). - The `--with-vscode-settings` and `--with-vscode-workspace` flags are hidden backward-compat aliases for the same VS Code helper and both write `.vscode/settings.json` and `.vscode/extensions.json`. - The `--with-intellij-settings` flag is a hidden backward-compat alias for generating `.idea/deno.xml`. diff --git a/apps/cli/src/commands/init/init.handler.ts b/apps/cli/src/commands/init/init.handler.ts index ea1c8163d6..520ed29ba6 100644 --- a/apps/cli/src/commands/init/init.handler.ts +++ b/apps/cli/src/commands/init/init.handler.ts @@ -2,6 +2,7 @@ import { Effect, Option, Path } from "effect"; import { RuntimeInfo } from "../../shared/runtime/runtime-info.service.ts"; import { initProject } from "../../shared/init/project-init.ts"; import { Output } from "../../shared/output/output.service.ts"; +import { resolveExperimentalFeature } from "../../command-internal/experimental-feature.ts"; import { ExperimentalFlag, WorkdirFlag, resolveYes } from "../../command-internal/global-flags.ts"; import { InitConfigExistsError, InitExperimentalRequiredError } from "./init.errors.ts"; import type { InitFlags } from "./init.command.ts"; @@ -19,6 +20,12 @@ export const init = Effect.fn("init")(function* (flags: InitFlags) { }); } + const experimentalStack = yield* resolveExperimentalFeature({ + feature: "stack", + configValue: Effect.succeed(false), + env: process.env, + }); + const result = yield* initProject({ cwd: Option.isSome(workdir) ? path.resolve(runtimeInfo.cwd, workdir.value) : runtimeInfo.cwd, force: flags.force, @@ -29,6 +36,7 @@ export const init = Effect.fn("init")(function* (flags: InitFlags) { yes: yield* resolveYes, withVscodeSettings: flags.withVscodeWorkspace || flags.withVscodeSettings, withIntellijSettings: flags.withIntellijSettings, + experimentalStack, }); if (!result.created) { diff --git a/apps/cli/src/commands/init/init.integration.test.ts b/apps/cli/src/commands/init/init.integration.test.ts index 8ab37d2a7e..74d4dfdb18 100644 --- a/apps/cli/src/commands/init/init.integration.test.ts +++ b/apps/cli/src/commands/init/init.integration.test.ts @@ -119,6 +119,8 @@ describe("init", () => { const content = yield* readTextFile(tempDir, "supabase", "config.toml"); expect(content).toContain("major_version = 17"); + expect(content).toContain("port = 54321"); + expect(content).not.toContain("stack = true"); expect(out.stdoutText).toBe("Finished supabase init.\n"); }); }); @@ -326,4 +328,69 @@ describe("init", () => { ); }); }); + + it.live("writes the stack-opt-in template when SUPABASE_EXPERIMENTAL_STACK=1", () => { + const tempDir = tempRoot.current; + + return withEnvVar( + "SUPABASE_EXPERIMENTAL_STACK", + "1", + Effect.gen(function* () { + const { layer, out } = setup(tempDir); + + yield* init({ ...BASE_INIT_FLAGS, interactive: false }).pipe(Effect.provide(layer)); + + const content = yield* readTextFile(tempDir, "supabase", "config.toml"); + expect(content).toContain("stack = true"); + expect(content).not.toMatch(/^shadow_port = 54320$/m); + expect(content).not.toMatch(/^port = 54321$/m); + expect(content).not.toMatch(/^inspector_port = 8083$/m); + expect(out.stdoutText).toBe("Finished supabase init.\n"); + }), + ); + }); + + it.live("keeps the established template when SUPABASE_EXPERIMENTAL_STACK=0", () => { + const tempDir = tempRoot.current; + + return withEnvVar( + "SUPABASE_EXPERIMENTAL_STACK", + "0", + Effect.gen(function* () { + const { layer } = setup(tempDir); + + yield* init({ ...BASE_INIT_FLAGS, interactive: false }).pipe(Effect.provide(layer)); + + const content = yield* readTextFile(tempDir, "supabase", "config.toml"); + expect(content).not.toContain("stack = true"); + expect(content).toContain("port = 54321"); + }), + ); + }); + + it.live("fails closed when SUPABASE_EXPERIMENTAL_STACK is not 0 or 1", () => { + const tempDir = tempRoot.current; + + return withEnvVar( + "SUPABASE_EXPERIMENTAL_STACK", + "yes", + Effect.gen(function* () { + const { layer } = setup(tempDir); + + const exit = yield* init({ ...BASE_INIT_FLAGS, interactive: false }).pipe( + Effect.provide(layer), + Effect.exit, + ); + + const error = findFailure(exit); + expect(error["_tag"]).toBe("ExperimentalFeatureFlagError"); + expect(error["message"]).toBe("SUPABASE_EXPERIMENTAL_STACK must be 0 or 1 when set"); + + expect(yield* renderFailureToStderr(exit)).toEqual([ + "SUPABASE_EXPERIMENTAL_STACK must be 0 or 1 when set\n", + "Try rerunning the command with --debug to troubleshoot the error.\n", + ]); + }), + ); + }); }); diff --git a/apps/cli/src/shared/init/project-init.templates.ts b/apps/cli/src/shared/init/project-init.templates.ts index b114e50c39..bd2ce51531 100644 --- a/apps/cli/src/shared/init/project-init.templates.ts +++ b/apps/cli/src/shared/init/project-init.templates.ts @@ -466,9 +466,53 @@ export const INTELLIJ_DENO_TEMPLATE = ` const ORIOLE_DB_VERSION = "15.1.0.150"; -export function renderCliConfigTemplate(projectId: string, useOrioledb: boolean): string { - return CONFIG_TEMPLATE_RAW.replace("__PROJECT_ID__", projectId).replace( +const EXPERIMENTAL_STACK_INIT_FLAG = `# Use the new local stack backend for top-level start, stop, and status. +stack = true +`; + +// Default ports omitted so the stack is not pinned to Docker-era values. +const STACK_INIT_OMITTED_PORT_BLOCKS = [ + `# Port to use for the API URL. +port = 54321 +`, + `# Port to use for the local database URL. +port = 54322 +`, + `# Port used by db diff command to initialize the shadow database. +shadow_port = 54320 +`, + `# Port to use for the local connection pooler. +port = 54329 +`, + `# Port to use for Supabase Studio. +port = 54323 +`, + `# Port to use for the email testing server web interface. +port = 54324 +`, + `# Port to attach the Chrome inspector for debugging edge functions. +inspector_port = 8083 +`, + `port = 54327 +`, +] as const; + +function applyExperimentalStackInitTemplate(source: string): string { + let next = source.replace("[experimental]\n", `[experimental]\n${EXPERIMENTAL_STACK_INIT_FLAG}`); + for (const block of STACK_INIT_OMITTED_PORT_BLOCKS) { + next = next.replace(block, ""); + } + return next; +} + +export function renderCliConfigTemplate( + projectId: string, + useOrioledb: boolean, + experimentalStack = false, +): string { + const rendered = CONFIG_TEMPLATE_RAW.replace("__PROJECT_ID__", projectId).replace( "__ORIOLEDB_VERSION__", useOrioledb ? ORIOLE_DB_VERSION : "", ); + return experimentalStack ? applyExperimentalStackInitTemplate(rendered) : rendered; } diff --git a/apps/cli/src/shared/init/project-init.templates.unit.test.ts b/apps/cli/src/shared/init/project-init.templates.unit.test.ts index aa8c1b88c4..b4f2347e3a 100644 --- a/apps/cli/src/shared/init/project-init.templates.unit.test.ts +++ b/apps/cli/src/shared/init/project-init.templates.unit.test.ts @@ -100,6 +100,23 @@ describe("project init templates", () => { expect(rendered).toContain("[experimental.pgdelta]\nenabled = true"); }); + it("opts the experimental stack template into stack=true without default listener ports", () => { + const rendered = renderCliConfigTemplate("demo-project", false, true); + expect(rendered).toMatch( + /\[experimental\]\n# Use the new local stack backend for top-level start, stop, and status.\nstack = true\n/, + ); + expect(rendered).toContain("# smtp_port = 54325"); + expect(rendered).toContain("[experimental.pgdelta]\nenabled = true"); + expect(rendered).not.toMatch(/^port = 54321$/m); + expect(rendered).not.toMatch(/^port = 54322$/m); + expect(rendered).not.toMatch(/^shadow_port = 54320$/m); + expect(rendered).not.toMatch(/^port = 54329$/m); + expect(rendered).not.toMatch(/^port = 54323$/m); + expect(rendered).not.toMatch(/^port = 54324$/m); + expect(rendered).not.toMatch(/^inspector_port = 8083$/m); + expect(rendered).not.toMatch(/^port = 54327$/m); + }); + it("matches the Go .gitignore scaffold", () => { expect(INIT_GITIGNORE_TEMPLATE).toBe(readVendoredTemplate("gitignore")); }); diff --git a/apps/cli/src/shared/init/project-init.ts b/apps/cli/src/shared/init/project-init.ts index 83a6afa168..a5d8e82b30 100644 --- a/apps/cli/src/shared/init/project-init.ts +++ b/apps/cli/src/shared/init/project-init.ts @@ -135,6 +135,8 @@ export interface ProjectInitOptions { readonly yes: boolean; readonly withVscodeSettings: boolean; readonly withIntellijSettings: boolean; + /** Persist `experimental.stack` and omit default stack listener ports. */ + readonly experimentalStack?: boolean; } // Files/directories are pinned to 0644/0755 explicitly rather than relying @@ -302,7 +304,7 @@ export const initProject = Effect.fnUntraced(function* (options: ProjectInitOpti yield* fs.makeDirectory(supabaseDir, { recursive: true, mode: INIT_DIR_MODE }); yield* fs.writeFileString( configTomlPath, - renderCliConfigTemplate(projectId, options.useOrioledb), + renderCliConfigTemplate(projectId, options.useOrioledb, options.experimentalStack === true), { mode: INIT_FILE_MODE }, ); yield* ensureSupabaseGitignore(options.cwd); From de56ecef95d6507cfe3c5ab5c7ff5fce60b97261 Mon Sep 17 00:00:00 2001 From: avallete Date: Thu, 17 Sep 2026 18:10:47 +0200 Subject: [PATCH 2/4] chore(cli): format init and bootstrap SIDE_EFFECTS tables --- apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md | 16 ++++++++-------- apps/cli/src/commands/init/SIDE_EFFECTS.md | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md b/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md index dd80ae60e2..6cc11e0654 100644 --- a/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md @@ -61,14 +61,14 @@ neither branch ever reaches the temp-login-role/Management-API path a passwordle ## Environment Variables -| Variable | Purpose | Required? | -| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -| `SUPABASE_WORKDIR` | target dir (`--workdir` flag → env → prompt → cwd) | no | -| `SUPABASE_DB_PASSWORD` | DB password (`-p` flag → env → prompt/generate) | no | -| `GITHUB_TOKEN` | raise the GitHub API rate limit for template fetch | no | -| `SUPABASE_ACCESS_TOKEN` | auth bypass for ensure-login | no | -| `SUPABASE_PROFILE` | profile name/path (env → `~/.supabase/profile` → `supabase`) | no | -| `SUPABASE_YES` | auto-confirm the native push step's prompts, read project-`.env`-aware like the standalone `db push` | no | +| Variable | Purpose | Required? | +| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | +| `SUPABASE_WORKDIR` | target dir (`--workdir` flag → env → prompt → cwd) | no | +| `SUPABASE_DB_PASSWORD` | DB password (`-p` flag → env → prompt/generate) | no | +| `GITHUB_TOKEN` | raise the GitHub API rate limit for template fetch | no | +| `SUPABASE_ACCESS_TOKEN` | auth bypass for ensure-login | no | +| `SUPABASE_PROFILE` | profile name/path (env → `~/.supabase/profile` → `supabase`) | no | +| `SUPABASE_YES` | auto-confirm the native push step's prompts, read project-`.env`-aware like the standalone `db push` | no | | `SUPABASE_EXPERIMENTAL_STACK` | blank/`scratch` path only; when `1`, the `initProject` scaffold persists `[experimental] stack = true` and omits Docker-era default ports. Invalid values fail closed | no | ## Exit Codes diff --git a/apps/cli/src/commands/init/SIDE_EFFECTS.md b/apps/cli/src/commands/init/SIDE_EFFECTS.md index c4e9fe6bec..b0716a4568 100644 --- a/apps/cli/src/commands/init/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/init/SIDE_EFFECTS.md @@ -28,8 +28,8 @@ ## Environment Variables -| Variable | Purpose | Required? | -| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | +| Variable | Purpose | Required? | +| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | | `SUPABASE_EXPERIMENTAL_STACK` | when `1`, persist `[experimental] stack = true` and omit Docker-era default ports from `supabase/config.toml`; when `0` or unset, write the established template | no | ## Exit Codes From 3e9daf61d02020906a656bae855d8bbc7a8c3d4d Mon Sep 17 00:00:00 2001 From: avallete Date: Thu, 17 Sep 2026 18:35:48 +0200 Subject: [PATCH 3/4] fix(cli): isolate experimental stack env from process.env Init tests were mutating SUPABASE_EXPERIMENTAL_STACK, which leaked into parallel integration files and could take pull off the mocked pg-delta path. --- .../command-internal/experimental-feature.ts | 17 +++- .../commands/bootstrap/bootstrap.command.ts | 4 +- .../commands/bootstrap/bootstrap.handler.ts | 7 +- apps/cli/src/commands/init/init.command.ts | 8 +- apps/cli/src/commands/init/init.handler.ts | 7 +- .../commands/init/init.integration.test.ts | 90 ++++++++++--------- 6 files changed, 81 insertions(+), 52 deletions(-) diff --git a/apps/cli/src/command-internal/experimental-feature.ts b/apps/cli/src/command-internal/experimental-feature.ts index 22e975d134..cf43e986b0 100644 --- a/apps/cli/src/command-internal/experimental-feature.ts +++ b/apps/cli/src/command-internal/experimental-feature.ts @@ -1,5 +1,5 @@ import { CliConfigSchema, findCliProjectPaths } from "@supabase/config/effect"; -import { Data, Effect, FileSystem, Option, Path, Schema } from "effect"; +import { Config, ConfigProvider, Data, Effect, FileSystem, Option, Path, Schema } from "effect"; import * as SmolToml from "smol-toml"; import { resolveWorkdir } from "../config/command-settings.layer.ts"; import { rootFlagTokens } from "../shared/cli/run.ts"; @@ -71,6 +71,21 @@ export const readExperimentalFeatureConfig = (input: { return decoded.experimental?.[input.feature]; }).pipe(Effect.orElseSucceed(() => undefined)); +/** + * Env record for one experimental feature, read from ConfigProvider. + */ +export const experimentalFeatureEnv = ( + feature: string, +): Effect.Effect>> => + Effect.gen(function* () { + const envName = `SUPABASE_EXPERIMENTAL_${feature.toUpperCase()}`; + const provider = yield* ConfigProvider.ConfigProvider; + const override = yield* Config.option(Config.string(envName)) + .parse(provider) + .pipe(Effect.orElseSucceed(() => Option.none())); + return { [envName]: Option.getOrUndefined(override) }; + }); + /** Resolves one experimental boolean from its environment override and config fallback. */ export const resolveExperimentalFeature = (input: { readonly feature: string; diff --git a/apps/cli/src/commands/bootstrap/bootstrap.command.ts b/apps/cli/src/commands/bootstrap/bootstrap.command.ts index 4798a0fe84..d4228b5ba2 100644 --- a/apps/cli/src/commands/bootstrap/bootstrap.command.ts +++ b/apps/cli/src/commands/bootstrap/bootstrap.command.ts @@ -1,7 +1,9 @@ +import { Layer } from "effect"; import { Argument, Command, Flag } from "effect/unstable/cli"; import type * as CliCommand from "effect/unstable/cli/Command"; import { withJsonErrorHandling } from "../../shared/output/json-error-handling.ts"; +import { cliConfigProviderLayer } from "../../shared/config/cli-config-provider.layer.ts"; import { withCommandTelemetry } from "../../telemetry/command-telemetry.ts"; import { bootstrapRuntimeLayer } from "./bootstrap.layers.ts"; import { bootstrap } from "./bootstrap.handler.ts"; @@ -27,5 +29,5 @@ export const bootstrapCommand = Command.make("bootstrap", config).pipe( // Go marks no bootstrap flag `markFlagTelemetrySafe`, so no `safeFlags`. bootstrap(flags).pipe(withCommandTelemetry({ flags }), withJsonErrorHandling), ), - Command.provide(bootstrapRuntimeLayer), + Command.provide(bootstrapRuntimeLayer.pipe(Layer.provideMerge(cliConfigProviderLayer))), ); diff --git a/apps/cli/src/commands/bootstrap/bootstrap.handler.ts b/apps/cli/src/commands/bootstrap/bootstrap.handler.ts index 324f4a3bd3..26ffa66092 100644 --- a/apps/cli/src/commands/bootstrap/bootstrap.handler.ts +++ b/apps/cli/src/commands/bootstrap/bootstrap.handler.ts @@ -33,7 +33,10 @@ import { projectCreateCore } from "../../command-internal/project-create-core.ts import { tempPaths } from "../../command-internal/temp-paths.ts"; import { extractServiceKeys } from "../../command-internal/tenant-keys.ts"; import { parseDotEnv } from "../../command-internal/dotenv.ts"; -import { resolveExperimentalFeature } from "../../command-internal/experimental-feature.ts"; +import { + experimentalFeatureEnv, + resolveExperimentalFeature, +} from "../../command-internal/experimental-feature.ts"; import { initProject } from "../../shared/init/project-init.ts"; import { buildDotEnv, marshalDotEnv } from "./bootstrap.dotenv.ts"; import { @@ -171,7 +174,7 @@ export const bootstrap = Effect.fn("bootstrap")(function* ( experimentalStack: yield* resolveExperimentalFeature({ feature: "stack", configValue: Effect.succeed(false), - env: process.env, + env: yield* experimentalFeatureEnv("stack"), }), }); } diff --git a/apps/cli/src/commands/init/init.command.ts b/apps/cli/src/commands/init/init.command.ts index 659387c067..df44722c64 100644 --- a/apps/cli/src/commands/init/init.command.ts +++ b/apps/cli/src/commands/init/init.command.ts @@ -2,6 +2,7 @@ import { Layer } from "effect"; import { Command, Flag } from "effect/unstable/cli"; import type * as CliCommand from "effect/unstable/cli/Command"; import { withJsonErrorHandling } from "../../shared/output/json-error-handling.ts"; +import { cliConfigProviderLayer } from "../../shared/config/cli-config-provider.layer.ts"; import { commandRuntimeLayer } from "../../shared/runtime/command-runtime.layer.ts"; import { stdinLayer } from "../../shared/runtime/stdin.layer.ts"; import { withCommandTelemetry } from "../../telemetry/command-telemetry.ts"; @@ -49,6 +50,9 @@ export const initCommand = Command.make("init", config).pipe( // `stdinLayer` satisfies `promptYesNo`'s `Stdin` requirement (via the // shared `initProject` IDE prompts). The prompts are gated on a TTY stdin, so // init never actually reads a piped line at runtime — the layer is here for - // the effect's type requirements only. - Command.provide(Layer.mergeAll(commandRuntimeLayer(["init"]), stdinLayer)), + // the effect's type requirements only. `cliConfigProviderLayer` satisfies + // experimental-stack env reads so ConfigProvider does not leak to the root. + Command.provide( + Layer.mergeAll(commandRuntimeLayer(["init"]), stdinLayer, cliConfigProviderLayer), + ), ); diff --git a/apps/cli/src/commands/init/init.handler.ts b/apps/cli/src/commands/init/init.handler.ts index 520ed29ba6..a10d0d2518 100644 --- a/apps/cli/src/commands/init/init.handler.ts +++ b/apps/cli/src/commands/init/init.handler.ts @@ -2,7 +2,10 @@ import { Effect, Option, Path } from "effect"; import { RuntimeInfo } from "../../shared/runtime/runtime-info.service.ts"; import { initProject } from "../../shared/init/project-init.ts"; import { Output } from "../../shared/output/output.service.ts"; -import { resolveExperimentalFeature } from "../../command-internal/experimental-feature.ts"; +import { + experimentalFeatureEnv, + resolveExperimentalFeature, +} from "../../command-internal/experimental-feature.ts"; import { ExperimentalFlag, WorkdirFlag, resolveYes } from "../../command-internal/global-flags.ts"; import { InitConfigExistsError, InitExperimentalRequiredError } from "./init.errors.ts"; import type { InitFlags } from "./init.command.ts"; @@ -23,7 +26,7 @@ export const init = Effect.fn("init")(function* (flags: InitFlags) { const experimentalStack = yield* resolveExperimentalFeature({ feature: "stack", configValue: Effect.succeed(false), - env: process.env, + env: yield* experimentalFeatureEnv("stack"), }); const result = yield* initProject({ diff --git a/apps/cli/src/commands/init/init.integration.test.ts b/apps/cli/src/commands/init/init.integration.test.ts index 74d4dfdb18..bf9f451fca 100644 --- a/apps/cli/src/commands/init/init.integration.test.ts +++ b/apps/cli/src/commands/init/init.integration.test.ts @@ -1,6 +1,16 @@ import { describe, expect, it } from "@effect/vitest"; import { BunServices } from "@effect/platform-bun"; -import { Cause, Effect, Exit, FileSystem, Layer, Option, Path, Stdio } from "effect"; +import { + Cause, + ConfigProvider, + Effect, + Exit, + FileSystem, + Layer, + Option, + Path, + Stdio, +} from "effect"; import { CliArgs } from "../../shared/cli/cli-args.service.ts"; import { ExperimentalFlag, WorkdirFlag, YesFlag } from "../../command-internal/global-flags.ts"; import { normalizeCause } from "../../shared/output/normalize-error.ts"; @@ -31,6 +41,7 @@ function setup( /** Piped stdin lines consumed by the non-TTY IDE-settings confirm reads. */ stdinInput?: string; platform?: NodeJS.Platform; + env?: Readonly>; } = {}, ) { const out = mockOutput({ format: "text", interactive: opts.interactive ?? false }); @@ -49,6 +60,9 @@ function setup( Layer.succeed(WorkdirFlag, opts.workdir ?? Option.none()), Layer.succeed(YesFlag, opts.yes ?? false), Layer.succeed(CliArgs, { args: [] }), + ConfigProvider.layer( + ConfigProvider.fromEnvRecord(opts.env ?? {}, { preserveEmptyStrings: true }), + ), ), }; } @@ -332,65 +346,53 @@ describe("init", () => { it.live("writes the stack-opt-in template when SUPABASE_EXPERIMENTAL_STACK=1", () => { const tempDir = tempRoot.current; - return withEnvVar( - "SUPABASE_EXPERIMENTAL_STACK", - "1", - Effect.gen(function* () { - const { layer, out } = setup(tempDir); + return Effect.gen(function* () { + const { layer, out } = setup(tempDir, { env: { SUPABASE_EXPERIMENTAL_STACK: "1" } }); - yield* init({ ...BASE_INIT_FLAGS, interactive: false }).pipe(Effect.provide(layer)); + yield* init({ ...BASE_INIT_FLAGS, interactive: false }).pipe(Effect.provide(layer)); - const content = yield* readTextFile(tempDir, "supabase", "config.toml"); - expect(content).toContain("stack = true"); - expect(content).not.toMatch(/^shadow_port = 54320$/m); - expect(content).not.toMatch(/^port = 54321$/m); - expect(content).not.toMatch(/^inspector_port = 8083$/m); - expect(out.stdoutText).toBe("Finished supabase init.\n"); - }), - ); + const content = yield* readTextFile(tempDir, "supabase", "config.toml"); + expect(content).toContain("stack = true"); + expect(content).not.toMatch(/^shadow_port = 54320$/m); + expect(content).not.toMatch(/^port = 54321$/m); + expect(content).not.toMatch(/^inspector_port = 8083$/m); + expect(out.stdoutText).toBe("Finished supabase init.\n"); + }); }); it.live("keeps the established template when SUPABASE_EXPERIMENTAL_STACK=0", () => { const tempDir = tempRoot.current; - return withEnvVar( - "SUPABASE_EXPERIMENTAL_STACK", - "0", - Effect.gen(function* () { - const { layer } = setup(tempDir); + return Effect.gen(function* () { + const { layer } = setup(tempDir, { env: { SUPABASE_EXPERIMENTAL_STACK: "0" } }); - yield* init({ ...BASE_INIT_FLAGS, interactive: false }).pipe(Effect.provide(layer)); + yield* init({ ...BASE_INIT_FLAGS, interactive: false }).pipe(Effect.provide(layer)); - const content = yield* readTextFile(tempDir, "supabase", "config.toml"); - expect(content).not.toContain("stack = true"); - expect(content).toContain("port = 54321"); - }), - ); + const content = yield* readTextFile(tempDir, "supabase", "config.toml"); + expect(content).not.toContain("stack = true"); + expect(content).toContain("port = 54321"); + }); }); it.live("fails closed when SUPABASE_EXPERIMENTAL_STACK is not 0 or 1", () => { const tempDir = tempRoot.current; - return withEnvVar( - "SUPABASE_EXPERIMENTAL_STACK", - "yes", - Effect.gen(function* () { - const { layer } = setup(tempDir); + return Effect.gen(function* () { + const { layer } = setup(tempDir, { env: { SUPABASE_EXPERIMENTAL_STACK: "yes" } }); - const exit = yield* init({ ...BASE_INIT_FLAGS, interactive: false }).pipe( - Effect.provide(layer), - Effect.exit, - ); + const exit = yield* init({ ...BASE_INIT_FLAGS, interactive: false }).pipe( + Effect.provide(layer), + Effect.exit, + ); - const error = findFailure(exit); - expect(error["_tag"]).toBe("ExperimentalFeatureFlagError"); - expect(error["message"]).toBe("SUPABASE_EXPERIMENTAL_STACK must be 0 or 1 when set"); + const error = findFailure(exit); + expect(error["_tag"]).toBe("ExperimentalFeatureFlagError"); + expect(error["message"]).toBe("SUPABASE_EXPERIMENTAL_STACK must be 0 or 1 when set"); - expect(yield* renderFailureToStderr(exit)).toEqual([ - "SUPABASE_EXPERIMENTAL_STACK must be 0 or 1 when set\n", - "Try rerunning the command with --debug to troubleshoot the error.\n", - ]); - }), - ); + expect(yield* renderFailureToStderr(exit)).toEqual([ + "SUPABASE_EXPERIMENTAL_STACK must be 0 or 1 when set\n", + "Try rerunning the command with --debug to troubleshoot the error.\n", + ]); + }); }); }); From fa6974759ac5b1490071cc3b3a9240e070425428 Mon Sep 17 00:00:00 2001 From: avallete Date: Thu, 17 Sep 2026 19:03:19 +0200 Subject: [PATCH 4/4] fix(cli): fail closed on blank-bootstrap stack env before filesystem writes Validate SUPABASE_EXPERIMENTAL_STACK on the scratch path before mkdir/chdir, cover that in integration tests, and document empty-string fallback. --- .../src/commands/bootstrap/SIDE_EFFECTS.md | 18 ++--- .../commands/bootstrap/bootstrap.handler.ts | 15 ++-- .../bootstrap/bootstrap.integration.test.ts | 71 ++++++++++++++++++- apps/cli/src/commands/init/SIDE_EFFECTS.md | 14 ++-- .../src/shared/init/project-init.templates.ts | 2 +- .../init/project-init.templates.unit.test.ts | 2 +- 6 files changed, 99 insertions(+), 23 deletions(-) diff --git a/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md b/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md index 6cc11e0654..26e46f18c5 100644 --- a/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md @@ -61,15 +61,15 @@ neither branch ever reaches the temp-login-role/Management-API path a passwordle ## Environment Variables -| Variable | Purpose | Required? | -| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -| `SUPABASE_WORKDIR` | target dir (`--workdir` flag → env → prompt → cwd) | no | -| `SUPABASE_DB_PASSWORD` | DB password (`-p` flag → env → prompt/generate) | no | -| `GITHUB_TOKEN` | raise the GitHub API rate limit for template fetch | no | -| `SUPABASE_ACCESS_TOKEN` | auth bypass for ensure-login | no | -| `SUPABASE_PROFILE` | profile name/path (env → `~/.supabase/profile` → `supabase`) | no | -| `SUPABASE_YES` | auto-confirm the native push step's prompts, read project-`.env`-aware like the standalone `db push` | no | -| `SUPABASE_EXPERIMENTAL_STACK` | blank/`scratch` path only; when `1`, the `initProject` scaffold persists `[experimental] stack = true` and omits Docker-era default ports. Invalid values fail closed | no | +| Variable | Purpose | Required? | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | +| `SUPABASE_WORKDIR` | target dir (`--workdir` flag → env → prompt → cwd) | no | +| `SUPABASE_DB_PASSWORD` | DB password (`-p` flag → env → prompt/generate) | no | +| `GITHUB_TOKEN` | raise the GitHub API rate limit for template fetch | no | +| `SUPABASE_ACCESS_TOKEN` | auth bypass for ensure-login | no | +| `SUPABASE_PROFILE` | profile name/path (env → `~/.supabase/profile` → `supabase`) | no | +| `SUPABASE_YES` | auto-confirm the native push step's prompts, read project-`.env`-aware like the standalone `db push` | no | +| `SUPABASE_EXPERIMENTAL_STACK` | blank/`scratch` path only; when `1`, persists `[experimental] stack = true` and omits Docker-era default ports. Empty is unset; other non-empty values fail closed | no | ## Exit Codes diff --git a/apps/cli/src/commands/bootstrap/bootstrap.handler.ts b/apps/cli/src/commands/bootstrap/bootstrap.handler.ts index 26ffa66092..9d0387307e 100644 --- a/apps/cli/src/commands/bootstrap/bootstrap.handler.ts +++ b/apps/cli/src/commands/bootstrap/bootstrap.handler.ts @@ -130,6 +130,15 @@ export const bootstrap = Effect.fn("bootstrap")(function* ( starter = allTemplates.find((t) => t.name === choice) ?? SCRATCH_TEMPLATE; } + const experimentalStack = + starter.url.length === 0 + ? yield* resolveExperimentalFeature({ + feature: "stack", + configValue: Effect.succeed(false), + env: yield* experimentalFeatureEnv("stack"), + }) + : false; + yield* fs.makeDirectory(workdir, { recursive: true }); const entries = yield* fs .readDirectory(workdir) @@ -171,11 +180,7 @@ export const bootstrap = Effect.fn("bootstrap")(function* ( useOrioledb: false, withVscodeSettings: false, withIntellijSettings: false, - experimentalStack: yield* resolveExperimentalFeature({ - feature: "stack", - configValue: Effect.succeed(false), - env: yield* experimentalFeatureEnv("stack"), - }), + experimentalStack, }); } diff --git a/apps/cli/src/commands/bootstrap/bootstrap.integration.test.ts b/apps/cli/src/commands/bootstrap/bootstrap.integration.test.ts index 2fd5e9b354..dea9a754ee 100644 --- a/apps/cli/src/commands/bootstrap/bootstrap.integration.test.ts +++ b/apps/cli/src/commands/bootstrap/bootstrap.integration.test.ts @@ -1,6 +1,17 @@ import { BunServices } from "@effect/platform-bun"; import { describe, expect, it } from "@effect/vitest"; -import { Cause, Effect, Exit, FileSystem, Layer, Option, Path, Redacted, Schedule } from "effect"; +import { + Cause, + ConfigProvider, + Effect, + Exit, + FileSystem, + Layer, + Option, + Path, + Redacted, + Schedule, +} from "effect"; import { mockAnalytics, @@ -95,6 +106,7 @@ interface SetupOpts { readonly dbPassword?: string; /** Raw `SUPABASE_WORKDIR` the settings captured; used verbatim, so no prompt fires. */ readonly workdirEnvValue?: string; + readonly env?: Readonly>; } function setup(path: Path.Path, opts: SetupOpts = {}) { @@ -237,6 +249,9 @@ function setup(path: Path.Path, opts: SetupOpts = {}) { Layer.succeed(NetworkIdFlag, Option.none()), Layer.succeed(CliArgs, { args: [] }), debugLoggerLayer.pipe(Layer.provide(Layer.succeed(DebugFlag, opts.debug ?? false))), + ConfigProvider.layer( + ConfigProvider.fromEnvRecord(opts.env ?? {}, { preserveEmptyStrings: true }), + ), ); return { @@ -289,6 +304,46 @@ describe("bootstrap integration", () => { }).pipe(Effect.provide(BunServices.layer)), ); + it.live( + "scratch scaffolding writes the stack-opt-in template when SUPABASE_EXPERIMENTAL_STACK=1", + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const s = setup(path, { env: { SUPABASE_EXPERIMENTAL_STACK: "1" } }); + yield* bootstrap(flags({ template: Option.some("scratch") }), FAST_BACKOFF).pipe( + Effect.provide(s.layer), + ); + const content = yield* fs.readFileString(path.join(s.workdir, "supabase", "config.toml")); + expect(content).toContain("stack = true"); + expect(content).not.toMatch(/^port = 54321$/m); + }).pipe(Effect.provide(BunServices.layer)), + ); + + it.live( + "scratch scaffolding fails closed on an invalid SUPABASE_EXPERIMENTAL_STACK before writing config", + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const s = setup(path, { env: { SUPABASE_EXPERIMENTAL_STACK: "yes" } }); + const exit = yield* Effect.exit( + bootstrap(flags({ template: Option.some("scratch") }), FAST_BACKOFF).pipe( + Effect.provide(s.layer), + ), + ); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(Cause.pretty(exit.cause)).toContain("ExperimentalFeatureFlagError"); + expect(Cause.pretty(exit.cause)).toContain( + "SUPABASE_EXPERIMENTAL_STACK must be 0 or 1 when set", + ); + } + expect(yield* fs.exists(path.join(s.workdir, "supabase", "config.toml"))).toBe(false); + expect(s.out.stderrText).not.toContain("Created a new project at"); + }).pipe(Effect.provide(BunServices.layer)), + ); + it.live("downloads a named template matched by argument", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; @@ -304,6 +359,20 @@ describe("bootstrap integration", () => { }).pipe(Effect.provide(BunServices.layer)), ); + it.live("ignores SUPABASE_EXPERIMENTAL_STACK on a downloaded template", () => + Effect.gen(function* () { + const path = yield* Path.Path; + const s = setup(path, { + samples: [NEXTJS_TEMPLATE], + env: { SUPABASE_EXPERIMENTAL_STACK: "yes" }, + }); + yield* bootstrap(flags({ template: Option.some("NextJS") }), FAST_BACKOFF).pipe( + Effect.provide(s.layer), + ); + expect(s.downloads).toHaveLength(1); + }).pipe(Effect.provide(BunServices.layer)), + ); + it.live("rejects an unknown template argument", () => Effect.gen(function* () { const path = yield* Path.Path; diff --git a/apps/cli/src/commands/init/SIDE_EFFECTS.md b/apps/cli/src/commands/init/SIDE_EFFECTS.md index b0716a4568..c9cdca403a 100644 --- a/apps/cli/src/commands/init/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/init/SIDE_EFFECTS.md @@ -28,9 +28,10 @@ ## Environment Variables -| Variable | Purpose | Required? | -| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -| `SUPABASE_EXPERIMENTAL_STACK` | when `1`, persist `[experimental] stack = true` and omit Docker-era default ports from `supabase/config.toml`; when `0` or unset, write the established template | no | +| Variable | Purpose | Required? | +| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | --------- | +| `SUPABASE_YES` | auto-accepts `-i` IDE prompts with the established stderr echo, same as `--yes` | no | +| `SUPABASE_EXPERIMENTAL_STACK` | when `1`, persist `[experimental] stack = true` and omit Docker-era default ports; `0`, unset, or empty writes the established template | no | ## Exit Codes @@ -38,7 +39,7 @@ | ---- | ------------------------------------------------------------------------------------ | | `0` | success - prints "Finished supabase init." | | `1` | `supabase/config.toml` already exists and `--force` was not provided | -| `1` | `SUPABASE_EXPERIMENTAL_STACK` is set to a value other than `0` or `1` | +| `1` | `SUPABASE_EXPERIMENTAL_STACK` is a non-empty value other than `0` or `1` | | `1` | permission denied writing config file | | `1` | an existing `.vscode/settings.json` / `.vscode/extensions.json` is not valid JSON(C) | @@ -79,7 +80,7 @@ required flag(s) "experimental" not set Try rerunning the command with --debug to troubleshoot the error. ``` -When `SUPABASE_EXPERIMENTAL_STACK` is set to a value other than `0` or `1` (stderr; the second line is the generic debug hint appended on error): +When `SUPABASE_EXPERIMENTAL_STACK` is a non-empty value other than `0` or `1` (stderr; the second line is the generic debug hint appended on error): ``` SUPABASE_EXPERIMENTAL_STACK must be 0 or 1 when set @@ -94,7 +95,8 @@ Try rerunning the command with --debug to troubleshoot the error. - `SUPABASE_EXPERIMENTAL_STACK=1` opts the new project into the experimental stack backend: the written config includes `[experimental] stack = true` and omits the Docker-era default ports (API, database, shadow, pooler, Studio, mail UI, Functions inspector, and Analytics). - Invalid values fail closed. `0` or unset keeps the established template. + A non-empty value other than `0` or `1` fails closed. `0`, unset, or empty keeps the + established template. - The `--interactive` / `-i` flag enables IDE settings prompts (only effective in TTY). - The `--with-vscode-settings` and `--with-vscode-workspace` flags are hidden backward-compat aliases for the same VS Code helper and both write `.vscode/settings.json` and `.vscode/extensions.json`. - The `--with-intellij-settings` flag is a hidden backward-compat alias for generating `.idea/deno.xml`. diff --git a/apps/cli/src/shared/init/project-init.templates.ts b/apps/cli/src/shared/init/project-init.templates.ts index bd2ce51531..683a44fc3a 100644 --- a/apps/cli/src/shared/init/project-init.templates.ts +++ b/apps/cli/src/shared/init/project-init.templates.ts @@ -466,7 +466,7 @@ export const INTELLIJ_DENO_TEMPLATE = ` const ORIOLE_DB_VERSION = "15.1.0.150"; -const EXPERIMENTAL_STACK_INIT_FLAG = `# Use the new local stack backend for top-level start, stop, and status. +const EXPERIMENTAL_STACK_INIT_FLAG = `# Use the new local stack backend for start, stop, and status, and for --local targets of db, migration, test db, gen types, inspect, and pull. stack = true `; diff --git a/apps/cli/src/shared/init/project-init.templates.unit.test.ts b/apps/cli/src/shared/init/project-init.templates.unit.test.ts index 29f4d13ef5..6dbb7073d6 100644 --- a/apps/cli/src/shared/init/project-init.templates.unit.test.ts +++ b/apps/cli/src/shared/init/project-init.templates.unit.test.ts @@ -98,7 +98,7 @@ describe("project init templates", () => { it("opts the experimental stack template into stack=true without default listener ports", () => { const rendered = renderCliConfigTemplate("demo-project", false, true); expect(rendered).toMatch( - /\[experimental\]\n# Use the new local stack backend for top-level start, stop, and status.\nstack = true\n/, + /\[experimental\]\n# Use the new local stack backend for start, stop, and status, and for --local targets of db, migration, test db, gen types, inspect, and pull.\nstack = true\n/, ); expect(rendered).toContain("# smtp_port = 54325"); expect(rendered).toContain("[experimental.pgdelta]\nenabled = true");