Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions apps/cli/docs/stack-commands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Local stack commands

`supabase stack` manages local stacks with the new runtime. It is available regardless of the project’s backend setting and supports both Docker and native runtimes.

| Command | Purpose |
| ------------------------ | ------------------------------------------- |
| `supabase stack start` | Create or resume the project’s stack. |
| `supabase stack stop` | Stop a stack while retaining its data. |
| `supabase stack status` | Inspect stack state and endpoints. |
| `supabase stack list` | List registered stacks. |
| `supabase stack logs` | Read or follow service logs. |
| `supabase stack prepare` | Prepare artifacts before starting services. |
| `supabase stack restart` | Restart an existing stack. |

Use each command’s `--help` for its available targeting and runtime options. The previous `supabase experimental stack` command path has been removed.

## Selecting the top-level commands

The top-level `supabase start`, `supabase stop`, and `supabase status` commands use the legacy backend by default. To make them aliases of the corresponding `supabase stack` commands, add this to `supabase/config.toml`:

```toml
[experimental]
stack = true
```

The selected backend determines accepted flags, help, and completion before the command is parsed. Set the flag to `false`, or remove it, to restore the legacy top-level commands. Explicit `supabase stack` commands continue to use the new backend.

For temporary selection, set `SUPABASE_EXPERIMENTAL_STACK=1` to select the new backend or `SUPABASE_EXPERIMENTAL_STACK=0` to select the legacy backend. This environment variable takes precedence over `experimental.stack` in `config.toml`; an unset or empty value falls back to the file setting. Other values are rejected. The override affects only the top-level lifecycle aliases and is applied before reading the project configuration.

This flag currently selects only the `start`, `stop`, and `status` aliases. It does not switch the database, migration, functions, or storage command families to the new backend.

## Data and configuration

The backends own separate state and databases. Enabling the flag does not import, copy, seed from, or reuse the legacy database, and does not stop a running legacy stack. Normal project migrations and seed configuration are separate from importing legacy database data.

The flag is local CLI configuration in `supabase/config.toml` and is excluded from hosted project configuration. When the environment override is absent or empty, lifecycle routing reads that exact file after applying the CLI’s working-directory rules, including `--workdir` and `SUPABASE_WORKDIR`; a JSON-only project does not enable the flag. Selecting a backend does not bypass validation when the selected command later loads its full configuration.
40 changes: 38 additions & 2 deletions apps/cli/src/cli/legacy-complete.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,12 @@ function makeCaptureTelemetry(
function makeDeps(
argv: ReadonlyArray<string>,
captureTelemetry: LegacyCompleteDeps["captureTelemetry"],
root: LegacyCompleteDeps["root"],
) {
const stdoutWrites: Array<string> = [];
const exits: Array<number> = [];
const deps: LegacyCompleteDeps = {
root: legacyRoot,
root,
argv,
env: {},
stdoutWrite: (message) => {
Expand All @@ -74,11 +75,45 @@ function makeDeps(
}

describe("legacy __complete telemetry (CLI-1965 review finding)", () => {
it.each(["__complete", "__completeNoDesc"])(
"keeps %s in completion handling when the selected command tree is unavailable",
async (completionCommand) => {
const analytics = mockAnalyticsWithContext();
const { deps, stdoutWrites, exits } = makeDeps(
[completionCommand, "stack", "st"],
makeCaptureTelemetry(analytics.layer),
undefined,
);

expect(await legacyTryComplete(deps)).toBe(true);
expect(stdoutWrites).toEqual([]);
expect(exits).toEqual([1]);
const event = analytics.captured.find((entry) => entry.event === EventCommandExecuted);
expect(event?.command).toBe("__complete");
expect(event?.properties[PropExitCode]).toBe(1);
},
);

it("leaves regular invocations unhandled when the selected command tree is unavailable", async () => {
const analytics = mockAnalyticsWithContext();
const { deps, stdoutWrites, exits } = makeDeps(
["stack", "status"],
makeCaptureTelemetry(analytics.layer),
undefined,
);

expect(await legacyTryComplete(deps)).toBe(false);
expect(stdoutWrites).toEqual([]);
expect(exits).toEqual([]);
expect(analytics.captured).toEqual([]);
});

it("fires cli_command_executed with command: __complete and exit_code: 0 for a normal completion request", async () => {
const analytics = mockAnalyticsWithContext();
const { deps } = makeDeps(
["__complete", "migration", "li"],
makeCaptureTelemetry(analytics.layer),
legacyRoot,
);

expect(await legacyTryComplete(deps)).toBe(true);
Expand All @@ -91,7 +126,7 @@ describe("legacy __complete telemetry (CLI-1965 review finding)", () => {

it("records exit_code: 1 for an unresolvable completion request (zero completion args)", async () => {
const analytics = mockAnalyticsWithContext();
const { deps } = makeDeps(["__complete"], makeCaptureTelemetry(analytics.layer));
const { deps } = makeDeps(["__complete"], makeCaptureTelemetry(analytics.layer), legacyRoot);

expect(await legacyTryComplete(deps)).toBe(true);

Expand All @@ -105,6 +140,7 @@ describe("legacy __complete telemetry (CLI-1965 review finding)", () => {
const { deps } = makeDeps(
["__completeNoDesc", "migration", "li"],
makeCaptureTelemetry(analytics.layer),
legacyRoot,
);

await legacyTryComplete(deps);
Expand Down
7 changes: 4 additions & 3 deletions apps/cli/src/cli/legacy-complete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export interface LegacyClassifyCompletionInput {
}

export interface LegacyCompleteDeps {
readonly root: Command.Command.Any;
readonly root: Command.Command.Any | undefined;
readonly argv: ReadonlyArray<string>;
readonly env: Readonly<Record<string, string | undefined>>;
readonly stdoutWrite: (message: string) => void;
Expand Down Expand Up @@ -1549,10 +1549,11 @@ export function legacyClassifyCompletion(
* (see the module doc comment for why that case isn't otherwise reproduced).
*/
export function legacyRespondToComplete(
root: Command.Command.Any,
root: Command.Command.Any | undefined,
argv: ReadonlyArray<string>,
): LegacyCompletionResult | undefined {
if (argv[0] !== "__complete" && argv[0] !== "__completeNoDesc") return undefined;
if (root === undefined) return undefined;

const args = argv.slice(1);
if (args.length === 0) return undefined;
Expand Down Expand Up @@ -1769,7 +1770,7 @@ export async function legacyTryComplete(deps: LegacyCompleteDeps): Promise<boole
return true;
}

export function legacyDefaultCompleteDeps(root: Command.Command.Any): LegacyCompleteDeps {
export function legacyDefaultCompleteDeps(root?: Command.Command.Any): LegacyCompleteDeps {
return {
root,
argv: process.argv.slice(2),
Expand Down
24 changes: 21 additions & 3 deletions apps/cli/src/cli/main.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
#!/usr/bin/env bun
import { BunServices } from "@effect/platform-bun";
import { Effect, Exit, Stdio } from "effect";
import { runCli } from "../shared/cli/run.ts";
import { legacyUpgradeNoticeHook } from "../command-internal/legacy-upgrade-notice.ts";
import { legacyAnalyticsLayer } from "../telemetry/legacy-analytics.layer.ts";
import { legacyDefaultCompleteDeps, legacyTryComplete } from "./legacy-complete.ts";
import { legacyRoot } from "./root.ts";
import { legacyResolveExperimentalStackBackend } from "../commands/experimental/stack/stack-backend.ts";
import { legacyRoot, legacyRootForBackend } from "./root.ts";

if (!(await legacyTryComplete(legacyDefaultCompleteDeps(legacyRoot)))) {
await runCli(legacyRoot, {
const args = await Effect.runPromise(
Effect.gen(function* () {
const stdio = yield* Stdio.Stdio;
return yield* stdio.args;
}).pipe(Effect.provide(BunServices.layer)),
);

const backendExit = await Effect.runPromiseExit(
legacyResolveExperimentalStackBackend({ args, cwd: process.cwd(), env: process.env }).pipe(
Effect.provide(BunServices.layer),
),
);
const root = Exit.isSuccess(backendExit) ? legacyRootForBackend(backendExit.value) : legacyRoot;
const completionRoot = Exit.isSuccess(backendExit) ? root : undefined;
if (!(await legacyTryComplete(legacyDefaultCompleteDeps(completionRoot)))) {
await runCli(root, {
analyticsLayer: legacyAnalyticsLayer,
afterSuccess: legacyUpgradeNoticeHook,
...(Exit.isFailure(backendExit) ? { beforeParse: Effect.failCause(backendExit.cause) } : {}),
});
}
Loading
Loading