Skip to content
Draft
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
15 changes: 8 additions & 7 deletions apps/cli/docs/go-cli-divergences.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ something the old Go CLI didn't — it is not a compatibility promise.

These commands exist in the TS CLI today but have no direct top-level equivalent in the old Go CLI reference.

| TS command | TS path | Notes |
| ----------------- | ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dev` | `planned` | Reserved for a TS-native long-running local development workflow command that watches files and orchestrates subcommands. Track this as TS-only unless a direct Go equivalent emerges. |
| `logs` | [`../src/next/commands/logs/logs.command.ts`](../src/next/commands/logs/logs.command.ts) | Streams local stack logs. No top-level `logs` command exists in the old Go CLI reference. |
| `api` | [`../src/next/commands/platform/api.command.ts`](../src/next/commands/platform/api.command.ts) | Low-level Management API client. It supersedes the old generated tree with explicit discovery via `supabase api routes` and execution via `supabase api request <route> [--method <METHOD>]`. |
| `stack` | [`../src/next/cli/root.ts`](../src/next/cli/root.ts) | TS-only local runtime namespace exposing `stack start`, `stack stop`, `stack status`, `stack list`, and `stack update`. Top-level `start`, `stop`, and `status` remain aliases. |
| `branches switch` | [`../src/next/commands/branches/switch/switch.command.ts`](../src/next/commands/branches/switch/switch.command.ts) | No direct Go equivalent. Updates local active-branch state so subsequent commands target the selected branch. |
| TS command | TS path | Notes |
| ----------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `dev` | `planned` | Reserved for a TS-native long-running local development workflow command that watches files and orchestrates subcommands. Track this as TS-only unless a direct Go equivalent emerges. |
| `logs` | [`../src/next/commands/logs/logs.command.ts`](../src/next/commands/logs/logs.command.ts) | Streams local stack logs. No top-level `logs` command exists in the old Go CLI reference. |
| `api` | [`../src/next/commands/platform/api.command.ts`](../src/next/commands/platform/api.command.ts) | Low-level Management API client. It supersedes the old generated tree with explicit discovery via `supabase api routes` and execution via `supabase api request <route> [--method <METHOD>]`. |
| `stack` | [`../src/next/cli/root.ts`](../src/next/cli/root.ts) | TS-only local runtime namespace exposing `stack start`, `stack stop`, `stack status`, `stack list`, and `stack update`. Top-level `start`, `stop`, and `status` remain aliases. |
| `branches switch` | [`../src/next/commands/branches/switch/switch.command.ts`](../src/next/commands/branches/switch/switch.command.ts) | No direct Go equivalent. Updates local active-branch state so subsequent commands target the selected branch. |
| `config diff` | [`../src/legacy/commands/config/diff/diff.command.ts`](../src/legacy/commands/config/diff/diff.command.ts) | Read-only drift report between `supabase/config.toml` and `GET /v2/projects/{ref}/config` (CLI-2156). TS-only: the old Go CLI had no config diff. `--target` accepts a branch name/UUID/ref; `--exit-code` exits 1 on drift. Rejects the Go-compat `-o/--output` flag outright — machine output is `--output-format json\|stream-json` only (no Go parity contract for net-new commands, per the CLI-2156 discussion). Comparison core lives in `@supabase/config` (ADR 0019). |

## Flag divergences from the Go reference

Expand Down
59 changes: 7 additions & 52 deletions apps/cli/src/legacy/commands/branches/branches.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,12 @@
import { Effect } from "effect";

import { LegacyPlatformApi } from "../../auth/legacy-platform-api.service.ts";
import { mapLegacyHttpError } from "../../shared/legacy-http-errors.ts";
import { legacyResolveBranchProjectRef as legacyResolveBranchProjectRefShared } from "../../shared/legacy-branch-ref.resolver.ts";
import {
LegacyBranchesFindNetworkError,
LegacyBranchesFindUnexpectedStatusError,
LegacyBranchesGetNetworkError,
LegacyBranchesGetUnexpectedStatusError,
} from "./branches.errors.ts";

/**
* Project ref pattern shared by every Management-API endpoint that accepts a
* 20-lowercase-letter project reference. Re-export so siblings (e.g.
* `get.handler.ts`) can classify branch-id inputs without re-declaring it.
*/
export const LEGACY_BRANCH_PROJECT_REF_PATTERN = /^[a-z]{20}$/;

/**
* Permissive UUID pattern (any 8-4-4-4-12 hex sequence) — accepts any RFC 4122
* variant including v6/v7 and version 0, matching the established liberal
* acceptance rather than the v1–v5 + variant-1 subset.
*/
export const LEGACY_BRANCH_UUID_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

const mapFindError = mapLegacyHttpError({
networkError: LegacyBranchesFindNetworkError,
statusError: LegacyBranchesFindUnexpectedStatusError,
Expand All @@ -39,38 +22,10 @@ const mapGetError = mapLegacyHttpError({
});

/**
* Resolves an arbitrary branch identifier to its project ref:
*
* 1. If the input matches `^[a-z]{20}$`, it's already a project ref — return as-is.
* 2. Else if the input is a UUID, call `V1GetABranchConfig` (`GET /v1/branches/{id}`)
* and return `JSON200.ref`.
* 3. Otherwise treat as a branch name under the linked project ref: call
* `V1GetABranch` (`GET /v1/projects/{ref}/branches/{name}`) and return
* `JSON200.project_ref`.
*
* The persistent `--project-ref` is required for path 3 and is passed in by
* the caller (which has already run `LegacyProjectRefResolver` so the linked
* project cache write does not re-fire here).
* The branches family's binding of the shared branch-ref resolver
* (`legacy/shared/legacy-branch-ref.resolver.ts`) to this family's error
* classes. See the shared module for resolution semantics.
*/
export const legacyResolveBranchProjectRef = Effect.fnUntraced(function* (
input: string,
projectRef: string,
) {
if (LEGACY_BRANCH_PROJECT_REF_PATTERN.test(input)) {
return input;
}

const api = yield* LegacyPlatformApi;

if (LEGACY_BRANCH_UUID_PATTERN.test(input)) {
const detail = yield* api.v1
.getABranchConfig({ branch_id_or_ref: input })
.pipe(Effect.catch(mapGetError));
return detail.ref;
}

const branch = yield* api.v1
.getABranch({ ref: projectRef, name: input })
.pipe(Effect.catch(mapFindError));
return branch.project_ref;
});
export function legacyResolveBranchProjectRef(input: string, projectRef: string) {
return legacyResolveBranchProjectRefShared(input, projectRef, { mapGetError, mapFindError });
}
2 changes: 1 addition & 1 deletion apps/cli/src/legacy/commands/branches/get/get.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { legacyPromptBranchId } from "../branches.prompt.ts";
import {
LEGACY_BRANCH_PROJECT_REF_PATTERN,
LEGACY_BRANCH_UUID_PATTERN,
} from "../branches.resolver.ts";
} from "../../../shared/legacy-branch-ref.resolver.ts";
import type { LegacyBranchesGetFlags } from "./get.command.ts";

type BranchDetail = typeof V1GetABranchConfigOutput.Type;
Expand Down
3 changes: 2 additions & 1 deletion apps/cli/src/legacy/commands/config/config.command.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Command } from "effect/unstable/cli";
import { legacyConfigDiffCommand } from "./diff/diff.command.ts";
import { legacyConfigPushCommand } from "./push/push.command.ts";

export const legacyConfigCommand = Command.make("config").pipe(
Command.withDescription("Manage Supabase project configurations."),
Command.withShortDescription("Manage project configurations"),
Command.withSubcommands([legacyConfigPushCommand]),
Command.withSubcommands([legacyConfigDiffCommand, legacyConfigPushCommand]),
);
107 changes: 107 additions & 0 deletions apps/cli/src/legacy/commands/config/diff/SIDE_EFFECTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# `supabase config diff`

Read-only comparison between the local `supabase/config.toml` and the effective
configuration the Management API reports for a target project or branch.
Classifies every remotely-managed property as `update` / `remote_only` /
`local_only` (unmanaged local-only properties are never reported). **Never
writes `config.toml` or any remote configuration.**

TS-only command — no Go CLI equivalent (see `docs/go-cli-divergences.md`).

## Files Read

| Path | Format | When |
| ---------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------ |
| `<workdir>/supabase/config.toml` | TOML | always, before any network call (missing file or parse error aborts, exit 1) |
| `<workdir>/supabase/.env`, `.env.local` | dotenv | always, to resolve `env(VAR)` references inside `config.toml` |
| `<workdir>/supabase/.temp/project-ref` | plain text | project-ref fallback (flag → `SUPABASE_PROJECT_ID` → this file); parent-ref for `--target` |
| `<workdir>/supabase/.temp/linked-project.json` | JSON | existence check only, for the telemetry cache write below |
| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable |

## Files Written

| Path | Format | When |
| ---------------------------------------------- | ------ | ---------------------------------------------------------------------- |
| `<workdir>/supabase/.temp/linked-project.json` | JSON | `Effect.ensuring` after run (success **and** failure), if ref resolved |
| `~/.supabase/telemetry.json` | JSON | `Effect.ensuring` after run (success **and** failure) |

**No writes to `supabase/config.toml` or `supabase/config.json`** — covered by
an integration test asserting mtime and contents are unchanged after a run
that finds differences.

## API Routes

All Bearer-authenticated, all read-only.

| # | Purpose | Method | Path | Success | Notes |
| --- | ---------------------------------- | ------ | ------------------------------------ | ------- | ---------------------------------------------------------------------- |
| 0a | branch by UUID (`--target <uuid>`) | GET | `/v1/branches/{branch_id}` | 200 | only when `--target` is a UUID |
| 0b | branch by name (`--target <name>`) | GET | `/v1/projects/{ref}/branches/{name}` | 200 | only when `--target` is not a ref/UUID; 404 → "branch not found" error |
| 1 | effective remote config | GET | `/v2/projects/{ref}/config` | 200 | always (after target resolution) |

## Environment Variables

| Variable | Purpose | Required? |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| `SUPABASE_PROJECT_ID` | project ref (flag → this → `.temp/project-ref` → prompt) | no |
| `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no (falls back to keyring → `~/.supabase/access-token`) |
| `SUPABASE_PROFILE` | API profile selection | no |
| `env(VAR)` references | interpolated into `config.toml` values at load; a change on an env-resolved property names the variable in the output | no |

## Exit Codes

| Code | Condition |
| ---- | ------------------------------------------------------------------------------ |
| `0` | success — including when differences are found, unless `--exit-code` is passed |
| `1` | `--exit-code` passed and at least one difference found |
| `1` | the Go-compat `-o/--output` global flag passed (any value — unsupported here) |
| `1` | missing or malformed `supabase/config.toml` |
| `1` | `--target` and `--project-ref` passed together |
| `1` | unknown branch (`--target` 404) |
| `1` | two `[remotes.*]` blocks declare the same `project_id` as the target ref |
| `1` | remote config read failure (network or unexpected status) |

## Output

Diagnostics on **stderr**: `Comparing against …` (resolved target + local
scope, i.e. `[remotes.<name>]` or `base config`) before the fetch, then
`Comparison scope: <blocks>` listing the blocks the response carried (missing
blocks are called out). The payload is on **stdout**.

### `--output-format text`

One block per difference (`<path> [update|remote only|local only]` with
`local:`/`remote:` lines; unset renders `(unset)` / `(not returned)`,
env-resolved values append `(from env VAR)`), then a summary count line —
`No config differences found.` when clean — and a
`Note: N credential value(s) not compared (masked by the API): …` line when
the file sets masked secrets.

### `--output-format json` / `stream-json`

`output.success(message, payload)` with the payload containing
`schema_version`, `target` (`project_ref`, optional `branch`, `local_scope`),
`scope`, `changes[]` (`path`, `class`, `local`, `remote`, optional
`env_variable`; unset sides are `null`), `masked[]`, and `counts`
(per class + `total`).

### `-o/--output` (Go-compat global flag)

**Not supported.** Any `-o` value — the machine formats and `pretty` alike —
fails fast (before target resolution or any network call) with
`the -o/--output flag is not supported by config diff; use --output-format
json|stream-json instead.` This is a net-new TS command with no Go parity
contract (CLI-2156 ticket discussion).

## Notes

- Run from the project root (or pass `--workdir`); `config.toml` is read relative to it.
- **Local operand per target (ADR 0018/0019):** when the resolved target ref matches a
`[remotes.<name>]` block's `project_id`, the local side is that branch's merged
effective config; otherwise the base config. The echoed scope line always says which.
- **Masked credentials:** secret-valued managed properties (the platform returns an HMAC,
never plaintext) are treated as "present, unknown" — never reported as differences and
never counted for `--exit-code`; they are surfaced via the masked note / `masked[]`.
- **Partial responses:** a managed property the response does not carry is `local_only`
when the file declares it and silent otherwise; a missing block is called out on the
scope line rather than treated as an error.
Loading