Skip to content
Merged
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
4 changes: 2 additions & 2 deletions apps/cli/docs/go-cli-porting-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,8 @@ Legend:
| `config push` | `wrapped` | [`../src/legacy/commands/config/push/push.command.ts`](../src/legacy/commands/config/push/push.command.ts) |
| `backups list` | `ported` | [`../src/legacy/commands/backups/list/list.command.ts`](../src/legacy/commands/backups/list/list.command.ts) |
| `backups restore` | `ported` | [`../src/legacy/commands/backups/restore/restore.command.ts`](../src/legacy/commands/backups/restore/restore.command.ts) |
| `snippets list` | `wrapped` | [`../src/legacy/commands/snippets/list/list.command.ts`](../src/legacy/commands/snippets/list/list.command.ts) |
| `snippets download` | `wrapped` | [`../src/legacy/commands/snippets/download/download.command.ts`](../src/legacy/commands/snippets/download/download.command.ts) |
| `snippets list` | `ported` | [`../src/legacy/commands/snippets/list/list.command.ts`](../src/legacy/commands/snippets/list/list.command.ts) |
| `snippets download` | `ported` | [`../src/legacy/commands/snippets/download/download.command.ts`](../src/legacy/commands/snippets/download/download.command.ts) |
| `sso list` | `ported` | [`../src/legacy/commands/sso/list/list.command.ts`](../src/legacy/commands/sso/list/list.command.ts) |
| `sso add` | `ported` | [`../src/legacy/commands/sso/add/add.command.ts`](../src/legacy/commands/sso/add/add.command.ts) |
| `sso remove` | `ported` | [`../src/legacy/commands/sso/remove/remove.command.ts`](../src/legacy/commands/sso/remove/remove.command.ts) |
Expand Down
27 changes: 0 additions & 27 deletions apps/cli/src/legacy/commands/backups/backups.format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,30 +22,3 @@ const REGION_NAMES: Readonly<Record<string, string>> = {
export function formatRegion(region: string): string {
return REGION_NAMES[region] ?? region;
}

function pad2(value: number): string {
return value.toString().padStart(2, "0");
}

/**
* Reproduces `utils.FormatTimestamp` from `apps/cli-go/internal/utils/render.go:17`:
* parse RFC3339; on success format as UTC "YYYY-MM-DD HH:MM:SS"; on failure
* return the input verbatim.
*/
export function formatBackupTimestamp(value: string): string {
if (value.length === 0) return value;
// Go uses time.Parse(time.RFC3339, value). Date.parse accepts a broader format
// surface, so we additionally require the year-month-day prefix to weed out
// values like "2026-02-08 16:44:07" (already-formatted) that Date.parse would
// happily accept but Go's strict RFC3339 parser would reject.
if (!/^\d{4}-\d{2}-\d{2}T/.test(value)) {
return value;
}
const parsed = Date.parse(value);
if (Number.isNaN(parsed)) return value;
const date = new Date(parsed);
return (
`${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())} ` +
`${pad2(date.getUTCHours())}:${pad2(date.getUTCMinutes())}:${pad2(date.getUTCSeconds())}`
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";

import { formatBackupTimestamp, formatRegion } from "./backups.format.ts";
import { formatRegion } from "./backups.format.ts";

describe("formatRegion", () => {
it.each([
Expand Down Expand Up @@ -30,26 +30,3 @@ describe("formatRegion", () => {
expect(formatRegion("xx-unknown-9")).toBe("xx-unknown-9");
});
});

describe("formatBackupTimestamp", () => {
it("formats valid RFC3339 to YYYY-MM-DD HH:MM:SS UTC", () => {
expect(formatBackupTimestamp("2026-02-08T16:44:07Z")).toBe("2026-02-08 16:44:07");
});

it("handles offsets by normalizing to UTC", () => {
expect(formatBackupTimestamp("2026-02-08T18:44:07+02:00")).toBe("2026-02-08 16:44:07");
});

it("falls back to the original value for already-formatted timestamps", () => {
// Go's time.Parse(time.RFC3339, ...) rejects "2026-02-08 16:44:07" (space, not T).
expect(formatBackupTimestamp("2026-02-08 16:44:07")).toBe("2026-02-08 16:44:07");
});

it("falls back for malformed input", () => {
expect(formatBackupTimestamp("not-a-timestamp")).toBe("not-a-timestamp");
});

it("returns empty string unchanged", () => {
expect(formatBackupTimestamp("")).toBe("");
});
});
5 changes: 3 additions & 2 deletions apps/cli/src/legacy/commands/backups/list/list.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import {
encodeYaml,
} from "../../../shared/legacy-go-output.encoders.ts";
import { mapLegacyHttpError } from "../../../shared/legacy-http-errors.ts";
import { formatBackupTimestamp, formatRegion } from "../backups.format.ts";
import { formatLegacyTimestamp } from "../../../shared/legacy-timestamp.format.ts";
import { formatRegion } from "../backups.format.ts";
import type { LegacyBackupsListFlags } from "./list.command.ts";

type BackupsResponse = typeof V1ListAllBackupsOutput.Type;
Expand Down Expand Up @@ -56,7 +57,7 @@ function renderLogicalTable(response: BackupsResponse): string {
region,
backup.is_physical_backup ? "PHYSICAL" : "LOGICAL",
backup.status,
formatBackupTimestamp(backup.inserted_at),
formatLegacyTimestamp(backup.inserted_at),
]);
return renderGlamourTable(LOGICAL_HEADERS, rows);
}
Expand Down
75 changes: 50 additions & 25 deletions apps/cli/src/legacy/commands/snippets/download/SIDE_EFFECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,59 +2,84 @@

## Files Read

| Path | Format | When |
| -------------------------- | ------------------------- | ---------------------------------------------------------- |
| `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable |
| Path | Format | When |
| ---------------------------------------------- | ------------------------- | --------------------------------------------------------------------------------------------- |
| keyring `"Supabase CLI"` / `<profile>` | OS keychain | when `SUPABASE_ACCESS_TOKEN` unset and keyring available; account = `LegacyCliConfig.profile` |
| keyring `"Supabase CLI"` / `access-token` | OS keychain | legacy-key fallback when the profile-keyed lookup misses |
| `~/.supabase/access-token` | plain text (token string) | last-resort fallback after env + keyring miss |
| `<workdir>/supabase/.temp/project-ref` | plain text | when `--project-ref` flag and `PROJECT_ID` env are unset |
| `<workdir>/supabase/.temp/linked-project.json` | JSON | always — `linkedProjectCache` reads to decide whether to write |

## Files Written

| Path | Format | When |
| ---- | ------ | ---- |
| — | — | — |
| Path | Format | When |
| ---------------------------------------------- | ------ | ------------------------------------------------------------------- |
| `~/.supabase/telemetry.json` | JSON | always (`Effect.ensuring(telemetryState.flush)`) |
| `<workdir>/supabase/.temp/linked-project.json` | JSON | best-effort after `--project-ref` resolves (Go `PersistentPostRun`) |

## API Routes

| Method | Path | Auth | Request body | Response (used fields) |
| ------ | ------------------- | ------------ | ------------ | -------------------------- |
| `GET` | `/v1/snippets/{id}` | Bearer token | none | `{content: {sql: string}}` |
| Method | Path | Auth | Request body | Response (used fields) |
| ------ | ------------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------ |
| `GET` | `/v1/snippets/{id}` | Bearer token | none | `{content: {sql, schema_version, favorite?}, id, name, visibility, owner, project, inserted_at, updated_at}` |

Only `content.sql` is rendered in text mode. The full payload is exposed via `--output-format json`.

## Environment Variables

| Variable | Purpose | Required? |
| ----------------------- | ---------------------------------------------------- | ------------------------------------------------------- |
| `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no (falls back to keyring → `~/.supabase/access-token`) |
| `PROJECT_ID` | project ref fallback when `--project-ref` is unset | no (falls back to `supabase/.temp/project-ref`) |
| `SUPABASE_API_URL` | override Management API base URL | no (defaults to `https://api.supabase.com`) |
| `SUPABASE_PROFILE` | profile selector (built-in name or YAML file path) | no (defaults to `supabase`) |

## Exit Codes

| Code | Condition |
| ---- | ------------------------------------------------------- |
| `0` | success — SQL content printed to stdout |
| `1` | invalid snippet ID argument (empty or not a valid UUID) |
| `1` | authentication error — no valid token found |
| `1` | API error — non-2xx response from `/v1/snippets/{id}` |
| `1` | network / connection failure |
| Code | Condition |
| ---- | ------------------------------------------------------------------- |
| `0` | success — SQL written to stdout |
| `1` | `LegacySnippetsInvalidIdError` — `<snippet-id>` is not a valid UUID |
| `1` | `LegacyInvalidProjectRefError` / `LegacyProjectNotLinkedError` |
| `1` | `LegacySnippetsDownloadUnexpectedStatusError` — non-2xx response |
| `1` | `LegacySnippetsDownloadNetworkError` — transport-level failure |

## Telemetry Events Fired

| Event | When | Notable properties |
| ---------------------- | ------------------------------------------ | ---------------------------------------------------------------------- |
| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` (`--project-ref` allowed verbatim) |

## Output

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

Prints the raw SQL content of the snippet to stdout, followed by a newline.
The raw SQL `content.sql` followed by a trailing `\n`.

```
select 1
select 1;
```

### `--output-format json`
### `--output-format json` (TS extension)

Not applicable — download writes SQL directly to stdout.
Single `success` event with the full `V1GetASnippetOutput` payload as `data`. This includes `id`, `name`, `visibility`, `owner`, `project`, `inserted_at`, `updated_at`, `favorite`, and `content` (with `sql`, `schema_version`, and optional `favorite`). Agents that only need the SQL can read `data.content.sql`; agents reconstructing a snippet in a new project have everything they need.

```json
{
"id": "0b0d48f6-…",
"name": "Create table",
"visibility": "user",
"owner": { "id": 7, "username": "supaseed" },
"content": { "schema_version": "1.0.0", "sql": "select 1;" }
}
```

### `--output-format stream-json`
### `--output-format stream-json` (TS extension)

Not applicable — download writes SQL directly to stdout.
NDJSON `success` event with the same full payload as `--output-format json`.

## Notes

- Requires a `<snippet-id>` positional argument (UUID).
- Requires `--project-ref` or a linked project.
- Phase 0 proxy: all invocations are forwarded to the bundled Go binary via `LegacyGoProxy`.
- Go's `--output` flag is **ignored** by `download.Run` — `fmt.Println(resp.JSON200.Content.Sql)` runs regardless of `pretty|json|yaml|toml|env`. The TS port mirrors this exactly: Go-style `--output` values do not change text-mode rendering. Only the TS-extension `--output-format json|stream-json` produces a structured payload.
- UUID validation runs **after** project-ref resolution but **before** the API call, matching Go's lifecycle: `PersistentPreRunE` resolves the ref first, then `download.Run` validates via `uuid.Parse`. Error messages mirror google/uuid v1.6.0: `invalid snippet ID: invalid UUID length: N` for malformed lengths, `invalid snippet ID: invalid UUID format` for length-36 inputs with wrong dash positions or hex chars.
- The linked-project cache fires after project-ref resolves (Go `PersistentPostRun`); the telemetry state always flushes (Go `Execute`). Both run on success and on every error path — including invalid-UUID early-exit — via the two `Effect.ensuring` blocks in the handler.
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
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 { legacyManagementApiRuntimeLayer } from "../../../shared/legacy-management-api-runtime.layer.ts";
import { withLegacyCommandInstrumentation } from "../../../telemetry/legacy-command-instrumentation.ts";
import { legacySnippetsDownload } from "./download.handler.ts";

const config = {
Expand All @@ -22,5 +26,14 @@ export const legacySnippetsDownloadCommand = Command.make("download", config).pi
description: "Download the SQL contents of the given snippet",
},
]),
Command.withHandler((flags) => legacySnippetsDownload(flags)),
Command.withHandler((flags) =>
legacySnippetsDownload(flags).pipe(
// No `safeFlags` — Go's `cmd/snippets.go` does not call
// `markFlagTelemetrySafe` for `--project-ref`, so the telemetry payload
// redacts the value.
withLegacyCommandInstrumentation({ flags }),
withJsonErrorHandling,
),
),
Command.provide(legacyManagementApiRuntimeLayer(["snippets", "download"])),
);
Loading
Loading