Skip to content

Commit 627e534

Browse files
authored
feat(cli): port backups list and restore to native TypeScript (#5331)
## Summary Replaces the Phase-0 Go-proxy handlers for `supabase backups list` and `supabase backups restore` with native Effect-based implementations. Adds the supporting legacy infrastructure (`legacy/auth`, `legacy/config`, project-ref resolver, Glamour table renderer) that subsequent ports will reuse. ## Highlights - **Strict Go parity on the wire.** Byte-identical `--output json` (alphabetical struct-field order, `backups: null` for empty slices to match Go's nil-slice semantics), Glamour-styled tables verified byte-for-byte against Go test fixtures, restore stderr line preserved. - **`Output.raw(text, stream)` service method.** Handlers now route stdout/stderr writes through the `Output` service instead of calling `process.stdout/stderr.write` directly. `mockOutput` captures these into `rawChunks` + `stdoutText` / `stderrText` getters, eliminating ~30 lines of `process.*.write` monkey-patching per integration test file. - **Shared backups infrastructure.** New `backups.layers.ts` exposes a `legacyBackupsRuntimeLayer(subcommand)` factory so each subcommand wires the platform-API + project-ref stack identically. New `mapLegacyBackupHttpError` factory in `backups.errors.ts` consolidates `RESPONSE_ERROR_TAGS` + HTTP-error dispatch and truncates response bodies to 1024 chars before embedding them in tagged errors. - **Flag-type discipline.** Both `*.command.ts` files mark `config as const` and `export type LegacyBackups*Flags = CliCommand.Command.Config.Infer<typeof config>` (canonical `login.command.ts` pattern); handlers import the type instead of duplicating private interfaces. - **Spinner suppressed in non-text modes.** `output.task("Fetching backups...")` / `"Initiating PITR restore..."` only run when `output.format === "text"`, eliminating dangling `[task] start:` lines on stderr in JSON / stream-json modes. - **API contracts regenerated.** `packages/api/src/generated/contracts.ts` rebuilt from upstream OpenAPI — adds the missing `id` field on backup items, plus broader spec drift since the last sync. - **Tests + checks pass.** Unit + integration suites green, including the new `backups.encoders.unit.test.ts` and the byte-stable `--output json` assertion (against the Go fixture from `apps/cli-go/internal/backups/list/list_test.go`). Targeted e2e `--help` smoke tests for both `list` and `restore`. ## Known Gaps (documented, not blocking) - `V1RestorePitrBackupInput.recovery_time_target_unix` retains an upstream `>= 0` constraint that Go's `int64` does not enforce. A negative timestamp surfaces a local schema-decode error rather than the API's own error. Noted in `restore/SIDE_EFFECTS.md`; resolving requires an upstream OpenAPI change. ## Reviewer Notes - The handlers do not log any token, error body, or response field that isn't already part of the documented Go output. Bodies are capped at 1024 chars even though the Management API is trusted, to set the right precedent for future ports against less-trusted endpoints. - The OpenAPI regen also touches `effect-client.ts` and `openapi.json`. Diff scope is large but mechanical — all the meaningful schema deltas land in `contracts.ts`. One unrelated `next/` snapshot expectation (`platform-schema.integration.test.ts`) updated to match the new upstream description text for `v1ListAllProjects`. Closes CLI-1301
1 parent 56b598b commit 627e534

51 files changed

Lines changed: 4622 additions & 522 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/cli/AGENTS.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,17 @@ Always check `src/shared/` before writing new infrastructure. Do not duplicate w
8888
| `shared/runtime/` | `Browser`, `Stdin`, `Tty`, `ProcessControl`, `RuntimeInfo` services + layers |
8989
| `shared/telemetry/` | `withCommandInstrumentation`, `Analytics`, tracing |
9090

91+
Also check the following `legacy/` infrastructure before writing equivalent helpers from scratch:
92+
93+
| Path | What it provides |
94+
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
95+
| `legacy/config/legacy-cli-config.layer.ts` | `LegacyCliConfig` — resolves `SUPABASE_PROFILE` (built-in name **or** YAML file path), `--workdir`, `--experimental`, project-id from `supabase/config.toml` |
96+
| `legacy/config/legacy-project-ref.layer.ts` | `LegacyProjectRefResolver``--project-ref` flag → env → linked-project.json → config fallback chain; matches Go's resolver order |
97+
| `legacy/telemetry/legacy-telemetry-state.layer.ts` | `LegacyTelemetryState.flush` — writes `~/.supabase/telemetry.json`, runs in every command's `Effect.ensuring` |
98+
| `legacy/telemetry/legacy-linked-project-cache.layer.ts` | `LegacyLinkedProjectCache.cache(ref)` — writes `~/.supabase/<workdir-hash>/linked-project.json` after `--project-ref` resolves; bypasses generated schema validation (uses raw HTTP client) |
99+
| `legacy/auth/legacy-http-debug.layer.ts` | `legacyHttpClientLayer` — wraps the HTTP transport with a `--debug` stderr logger in Go's `log.LstdFlags` format |
100+
| `legacy/output/legacy-glamour-table.ts` | `renderGlamourTable(headers, rows)` — byte-exact ASCII match for Go's `glamour.RenderTable(..., AsciiStyle)` |
101+
91102
---
92103

93104
## Phase 0: Go Binary Wrapper
@@ -139,6 +150,21 @@ src/legacy/commands/<command>/
139150
SIDE_EFFECTS.md # Required for every legacy command — see section below
140151
```
141152

153+
When a command grows beyond a single handler file, follow the optional helper-file shape that emerged from the backups port:
154+
155+
```
156+
src/legacy/commands/<command>/
157+
<command>.command.ts # Effect CLI Command + flag wiring + layer provide
158+
<command>.handler.ts # native Effect handler
159+
<command>.errors.ts # Data.TaggedError types
160+
<command>.layers.ts # runtime layer composition for the command family
161+
<command>.format.ts # text formatters (timestamps, regions, booleans)
162+
<command>.encoders.ts # Go-compatible JSON / YAML / TOML / env encoders
163+
SIDE_EFFECTS.md
164+
```
165+
166+
The `.format.ts` and `.encoders.ts` files should be pure functions with no Effect or service dependencies — that keeps them unit-testable and makes Go-parity rules explicit (e.g. JSON key sort order, env-var SCREAMING_SNAKE_CASE flattening, empty arrays coerced to null).
167+
142168
Commands with subcommands use nested directories:
143169

144170
```
@@ -192,6 +218,27 @@ Many Management API commands in `next/commands/` have already been implemented.
192218

193219
---
194220

221+
## Legacy Port: Hoist Before You Duplicate
222+
223+
Before writing handler code for a new port, scan the already-ported commands for overlapping logic. If two commands need the same helper (HTTP-error mapping, output encoder, formatter, runtime layer composition), hoist it instead of inlining a copy.
224+
225+
Decision rule:
226+
227+
- **Used by one command only** → keep it in the command's own directory (e.g. `backups/backups.errors.ts`).
228+
- **Used by ≥2 commands in the same command family** → keep it in the family root (e.g. `backups/backups.encoders.ts` is shared by `list` and `restore`).
229+
- **Used by ≥2 commands across families** → hoist to `src/legacy/shared/` (create the directory if it doesn't exist) and refactor the existing call sites in the same change. Do not leave the older command using its inlined copy while the new command uses the hoisted version.
230+
231+
Concrete examples worth watching for as more commands land:
232+
233+
- HTTP-error → tagged-error mapping (`backups.errors.ts:mapLegacyBackupHttpError`) — almost every Management API command will need this shape.
234+
- Go-compatible JSON / YAML / TOML / env encoders (`backups.encoders.ts`) — the flag `--output {json,yaml,toml,env}` is supported by many Go subcommands.
235+
- Glamour-table rendering helpers and column padding — currently in `legacy/output/legacy-glamour-table.ts`, already correctly hoisted.
236+
- Timestamp / region / boolean formatters (`backups.format.ts`) — likely shared the moment a second command renders a backup/project/region field.
237+
238+
This rule is consistent with the repo-wide **Refactoring Policy** ("delete obsolete helpers, shims, and parallel code paths as part of the refactor") — it just makes the policy concrete for the legacy-port workflow.
239+
240+
---
241+
195242
## Legacy Port: Go CLI Output Parity
196243

197244
The legacy shell is a **strict 1:1 port** — not a redesign. The compatibility contract covers:
@@ -206,6 +253,24 @@ When in doubt about expected output or behavior, run the equivalent command agai
206253

207254
---
208255

256+
## Legacy Port: Go Parity Checklist
257+
258+
When porting a Management-API-style command, verify each item before marking the command as `ported`:
259+
260+
1. **Telemetry + linked-project writes run on every invocation** — Go uses `PersistentPostRun` (see `apps/cli-go/cmd/root.go:176`). Wrap the handler body in `.pipe(Effect.ensuring(linkedProjectCache.cache(ref)), Effect.ensuring(telemetryState.flush))` so both files are written on success **and** failure. See `backups/list/list.handler.ts:74-114` as the canonical pattern.
261+
262+
2. **Errors go to stderr in text mode, byte-matching Go's template**`Output.fail` now writes a frame-free message to stderr followed by the "Try rerunning the command with --debug to get more details." suggestion when `--debug` is unset. Don't reintroduce clack's `■ … │` frame. Reference: commits `ee041834`, `cf4f574b`.
263+
264+
3. **`--debug` logs every HTTP request on stderr** — Format `"HTTP YYYY/MM/DD HH:MM:SS <METHOD>: <URL>\n"` (Go's `log.LstdFlags|log.Lmsgprefix`). Provided automatically by `legacyHttpClientLayer`; ensure that layer (not the raw `HttpClient.layer`) is what every legacy command's runtime composes. Reference: commit `39cfec20`.
265+
266+
4. **`SUPABASE_PROFILE` is dual-mode** — accept either a built-in name (`supabase`, `supabase-staging`, `supabase-local`) **or** a filesystem path to a YAML file with `api_url:` / `gotrue_url:` / `db_url:` keys. cli-e2e harness relies on the file-path mode. Reference: commit `288c2937`.
267+
268+
5. **`Layer.provide` does not share to siblings inside `Layer.mergeAll`** — if two sibling layers each require `LegacyCliConfig`, provide it to both explicitly. Smoke-test the bundled binary (`bun run build && ./dist/supabase-legacy …`) when changing production layer wiring; in-process tests don't always catch the missing-service panic. Reference: commit `a816b12e`, `backups.layers.ts:32-46`.
269+
270+
6. **Both `--output` (Go) and `--output-format` (TS) must be honored** — Go's `--output` (`pretty|json|yaml|toml|env`) takes priority when set. Pattern in `backups/list/list.handler.ts:85-113`: branch on `goOutputFlag` first, then fall through to TS `--output-format` text/json/stream-json.
271+
272+
---
273+
209274
## Legacy Port: File Location Compatibility
210275

211276
The legacy shell bridges two worlds: it must behave exactly like the Go CLI for existing users, and it must lay the groundwork for a seamless upgrade to the next shell.
@@ -311,6 +376,7 @@ Read https://www.effect.solutions/testing for Effect testing patterns. Note that
311376
- If a test needs multiple service replacements or `Layer.mergeAll(...)`, it likely belongs in `*.integration.test.ts`.
312377
- Prefer assertions on outputs and accumulated state over spy-heavy interaction tests.
313378
- Keep `*.e2e.test.ts` focused on golden paths, CLI surface behavior, and subprocess correctness, not branch-by-branch coverage.
379+
- **Forbidden pattern (do not add):** spawning the CLI to assert that `--help` renders a flag. Help text is dynamic over flag wiring and is exercised by the integration test's flag parser. The two backups e2e files removed alongside this guidance update are the canonical example of what not to write.
314380

315381
---
316382

0 commit comments

Comments
 (0)