Skip to content
Open
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
10 changes: 5 additions & 5 deletions apps/cli/docs/go-cli-porting-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,11 +241,11 @@ Legend:
| `sso update` | `ported` | [`../src/legacy/commands/sso/update/update.command.ts`](../src/legacy/commands/sso/update/update.command.ts) |
| `sso show` | `ported` | [`../src/legacy/commands/sso/show/show.command.ts`](../src/legacy/commands/sso/show/show.command.ts) |
| `sso info` | `ported` | [`../src/legacy/commands/sso/info/info.command.ts`](../src/legacy/commands/sso/info/info.command.ts) |
| `domains create` | `wrapped` | [`../src/legacy/commands/domains/create/create.command.ts`](../src/legacy/commands/domains/create/create.command.ts) |
| `domains get` | `wrapped` | [`../src/legacy/commands/domains/get/get.command.ts`](../src/legacy/commands/domains/get/get.command.ts) |
| `domains reverify` | `wrapped` | [`../src/legacy/commands/domains/reverify/reverify.command.ts`](../src/legacy/commands/domains/reverify/reverify.command.ts) |
| `domains activate` | `wrapped` | [`../src/legacy/commands/domains/activate/activate.command.ts`](../src/legacy/commands/domains/activate/activate.command.ts) |
| `domains delete` | `wrapped` | [`../src/legacy/commands/domains/delete/delete.command.ts`](../src/legacy/commands/domains/delete/delete.command.ts) |
| `domains create` | `ported` | [`../src/legacy/commands/domains/create/create.command.ts`](../src/legacy/commands/domains/create/create.command.ts) |
| `domains get` | `ported` | [`../src/legacy/commands/domains/get/get.command.ts`](../src/legacy/commands/domains/get/get.command.ts) |
| `domains reverify` | `ported` | [`../src/legacy/commands/domains/reverify/reverify.command.ts`](../src/legacy/commands/domains/reverify/reverify.command.ts) |
| `domains activate` | `ported` | [`../src/legacy/commands/domains/activate/activate.command.ts`](../src/legacy/commands/domains/activate/activate.command.ts) |
| `domains delete` | `ported` | [`../src/legacy/commands/domains/delete/delete.command.ts`](../src/legacy/commands/domains/delete/delete.command.ts) |
| `vanity-subdomains get` | `ported` | [`../src/legacy/commands/vanity-subdomains/get/get.command.ts`](../src/legacy/commands/vanity-subdomains/get/get.command.ts) |
| `vanity-subdomains check-availability` | `ported` | [`../src/legacy/commands/vanity-subdomains/check-availability/check-availability.command.ts`](../src/legacy/commands/vanity-subdomains/check-availability/check-availability.command.ts) |
| `vanity-subdomains activate` | `ported` | [`../src/legacy/commands/vanity-subdomains/activate/activate.command.ts`](../src/legacy/commands/vanity-subdomains/activate/activate.command.ts) |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ function mockCliConfig(opts: { accessToken?: string; apiUrl?: string; userAgent?
return Layer.succeed(LegacyCliConfig, {
profile: "supabase",
apiUrl: opts.apiUrl ?? "https://api.supabase.com",
projectHost: "supabase.co",
accessToken:
opts.accessToken === undefined ? Option.none() : Option.some(Redacted.make(opts.accessToken)),
projectId: Option.none(),
Expand Down
101 changes: 101 additions & 0 deletions apps/cli/src/legacy/commands/domains/SIDE_EFFECTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# `supabase domains [create|get|reverify|activate|delete]`

Custom hostname management. Every subcommand resolves a project ref and calls a
single Management API custom-hostname endpoint. `create` additionally performs a
Cloudflare DNS-over-HTTPS CNAME pre-check.

## Files Read

| Path | Format | When |
| -------------------------------------- | ------------------------- | ---------------------------------------------------------- |
| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable |
| `<workdir>/supabase/.temp/project-ref` | plain text | when `--project-ref` flag and `PROJECT_ID` env are unset |

## Files Written

| Path | Format | When |
| ------------------------------------------------ | ------ | -------------------------------------------------- |
| `~/.supabase/<workdir-hash>/linked-project.json` | JSON | always (PersistentPostRun), after the ref resolves |
| `~/.supabase/telemetry.json` | JSON | always (PersistentPostRun), success or failure |

## API Routes

| Method | Path | Auth | Request body | Response (used fields) |
| -------- | ----------------------------------------------- | ------------ | --------------------------- | -------------------------------------------------------------------------------------------- |
| `GET` | `https://1.1.1.1/dns-query?name=<host>&type=5` | none | none | `{Answer: [{type, data}]}` — first CNAME answer (`create` only, before the POST) |
| `POST` | `/v1/projects/{ref}/custom-hostname/initialize` | Bearer token | `{custom_hostname: string}` | 201 → `{status, custom_hostname, data: {result: {ssl, custom_origin_server, …}}}` (`create`) |
| `GET` | `/v1/projects/{ref}/custom-hostname` | Bearer token | none | 200 → same response shape (`get`) |
| `POST` | `/v1/projects/{ref}/custom-hostname/reverify` | Bearer token | none | 201 → same response shape (`reverify`) |
| `POST` | `/v1/projects/{ref}/custom-hostname/activate` | Bearer token | none | 201 → same response shape (`activate`) |
| `DELETE` | `/v1/projects/{ref}/custom-hostname` | Bearer token | none | 200 → empty/void body (`delete`) |

## Environment Variables

| Variable | Purpose | Required? |
| ----------------------- | ---------------------------------------------------- | -------------------------------------------------------- |
| `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no (falls back to keyring → `~/.supabase/access-token`) |
| `SUPABASE_PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (falls back to linked-project file) |
| `SUPABASE_PROFILE` | built-in profile name or path to a YAML profile | no (defaults to `supabase`; sets API URL + project host) |

## Exit Codes

| Code | Condition |
| ---- | ------------------------------------------------------------------------------- |
| `0` | success |
| `1` | project ref cannot be resolved / is malformed |
| `1` | `create`: hostname has no matching CNAME record (Cloudflare DNS pre-check) |
| `1` | API error — unexpected (non-201/200) response from the custom-hostname endpoint |
| `1` | network / connection failure |

## Telemetry Events Fired

| Event | When | Notable properties / groups |
| ---------------------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------- |
| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` (all redacted — Go marks no `domains` flag telemetry-safe) |

No custom events: the Go `internal/hostnames` package emits no `phtelemetry.*` calls.

## Output

In `pretty`/text mode the `PrintStatus` text is written to **stderr** and nothing
to stdout. In a structured `-o` mode (`json`/`yaml`/`toml`/`env`) the encoded
response goes to **stdout** and the human status is suppressed on stderr (see the
divergence note below). `delete` only prints a fixed success line to stderr and
ignores `-o`.

### `--output-format text` (Go CLI compatible)

stderr carries the status message for the hostname's current state, e.g.:

```
Custom hostname configuration not started.
```

`delete` prints `Deleted custom hostname config successfully.` to stderr. stdout is empty in text mode.

### `--output-format json`

Single JSON object (the full custom-hostname response) emitted via `output.success`.
`delete` emits `{}` with the success message.

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

A `result` event carrying the custom-hostname response object.

### Go `-o {json,yaml,toml,env}`

When the Go `--output`/`-o` flag is set (or `--include-raw-output` forces `json`),
the full response is encoded to stdout in that format and the human status is
suppressed on stderr. `delete` ignores `-o`.

## Notes

- `--custom-hostname` is required for `create`.
- `create` validates the CNAME via Cloudflare DNS-over-HTTPS (`https://1.1.1.1`, 10s timeout) before initializing; on failure it short-circuits before any POST.
- All subcommands resolve the ref via `--project-ref` → `PROJECT_ID` env → linked-project file, matching Go.
- The project-ref fallback env var is `SUPABASE_PROJECT_ID`, matching Go (Go calls `viper.GetString("PROJECT_ID")` under `viper.SetEnvPrefix("SUPABASE")`, which resolves to the `SUPABASE_PROJECT_ID` environment variable).
- **Documented divergences from Go (intentional):**
- `--include-raw-output` is declared as a normal boolean **on each subcommand** (Go declares it as a persistent flag on the `domains` group). Two consequences: (a) it must appear after the subcommand name (`domains get --include-raw-output`) rather than before it (`domains --include-raw-output get`), matching how `--project-ref` is already handled shell-wide; (b) it cannot reproduce Cobra's help-hiding or the `Flag --include-raw-output has been deprecated` stderr warning, which Effect CLI has no hook for. It still reproduces the behavioral effect (forces `-o json` when `-o` is unset/pretty); on `delete` it is inert, matching Go.
- `-o json|yaml|toml|env` encode the decoded snake_case response, not Go's PascalCase struct keys (consistent with `backups list` / `sso add`).
- The degenerate `validation_records != 1` status message approximates Go's `%+v` struct dump (which embeds a non-deterministic pointer address).
- In a structured `-o` mode the human status is suppressed on stderr. Go technically still writes `PrintStatus` to stderr, but the `5_*`/`4_*` messages carry no trailing newline, so they fuse with Go's version-update notice and are stripped together by the e2e normalizer — making Go's observable machine-output stderr empty. Suppressing keeps stdout clean and matches the parity contract.
42 changes: 0 additions & 42 deletions apps/cli/src/legacy/commands/domains/activate/SIDE_EFFECTS.md

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
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 { legacyManagementApiRuntimeLayer } from "../../../shared/legacy-management-api-runtime.layer.ts";
import { withLegacyCommandInstrumentation } from "../../../telemetry/legacy-command-instrumentation.ts";
import { legacyDomainsActivate } from "./activate.handler.ts";

const config = {
projectRef: Flag.string("project-ref").pipe(
Flag.withDescription("Project ref of the Supabase project."),
Flag.optional,
),
includeRawOutput: Flag.boolean("include-raw-output").pipe(
Flag.withDescription("(Deprecated) use -o json instead."),
),
} as const;

export type LegacyDomainsActivateFlags = CliCommand.Command.Config.Infer<typeof config>;
Expand All @@ -22,5 +29,11 @@ export const legacyDomainsActivateCommand = Command.make("activate", config).pip
description: "Activate the custom hostname for a project",
},
]),
Command.withHandler((flags) => legacyDomainsActivate(flags)),
Command.withHandler((flags) =>
legacyDomainsActivate(flags).pipe(
withLegacyCommandInstrumentation({ flags }),
withJsonErrorHandling,
),
),
Command.provide(legacyManagementApiRuntimeLayer(["domains", "activate"])),
);
Original file line number Diff line number Diff line change
@@ -1,12 +1,36 @@
import { Effect, Option } from "effect";
import { LegacyGoProxy } from "../../../../shared/legacy/go-proxy.service.ts";
import { Effect } from "effect";

import { LegacyPlatformApi } from "../../../auth/legacy-platform-api.service.ts";
import { LegacyProjectRefResolver } from "../../../config/legacy-project-ref.service.ts";
import { Output } from "../../../../shared/output/output.service.ts";
import { LegacyLinkedProjectCache } from "../../../telemetry/legacy-linked-project-cache.service.ts";
import { LegacyTelemetryState } from "../../../telemetry/legacy-telemetry-state.service.ts";
import { emitLegacyHostnameResult } from "../domains.emit.ts";
import { mapLegacyDomainsHttpError } from "../domains.errors.ts";
import type { LegacyDomainsActivateFlags } from "./activate.command.ts";

const mapActivateError = mapLegacyDomainsHttpError("activate");

export const legacyDomainsActivate = Effect.fn("legacy.domains.activate")(function* (
flags: LegacyDomainsActivateFlags,
) {
const proxy = yield* LegacyGoProxy;
const args: string[] = ["domains", "activate"];
if (Option.isSome(flags.projectRef)) args.push("--project-ref", flags.projectRef.value);
yield* proxy.exec(args);
const output = yield* Output;
const api = yield* LegacyPlatformApi;
const resolver = yield* LegacyProjectRefResolver;
const linkedProjectCache = yield* LegacyLinkedProjectCache;
const telemetryState = yield* LegacyTelemetryState;

const ref = yield* resolver.resolve(flags.projectRef);

yield* Effect.gen(function* () {
const activating =
output.format === "text" ? yield* output.task("Activating custom hostname...") : undefined;
const response = yield* api.v1.activateCustomHostname({ ref }).pipe(
Effect.tapError(() => activating?.fail() ?? Effect.void),
Effect.catch(mapActivateError),
);
yield* activating?.clear() ?? Effect.void;

yield* emitLegacyHostnameResult(response, flags.includeRawOutput);
}).pipe(Effect.ensuring(linkedProjectCache.cache(ref)), Effect.ensuring(telemetryState.flush));
});
Loading
Loading