Skip to content

feat(cli): port backups list and restore to native TypeScript#5331

Open
Coly010 wants to merge 8 commits into
developfrom
cli/port-backups-command
Open

feat(cli): port backups list and restore to native TypeScript#5331
Coly010 wants to merge 8 commits into
developfrom
cli/port-backups-command

Conversation

@Coly010
Copy link
Copy Markdown
Contributor

@Coly010 Coly010 commented May 21, 2026

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

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 (auth, config, project-ref
resolution, Glamour table renderer) that future ports will reuse.

- Strict Go parity on stdout/stderr: byte-identical `--output json` (alphabetical
  field order, `backups: null` for nil slices), Glamour tables, restore stderr line.
- Extends `Output` with a `raw(text, stream)` method so handlers route stdout/stderr
  through the service. Removes monkey-patching from integration tests.
- Hoists shared backups layer composition (`backups.layers.ts`) and HTTP error
  mapping (`mapLegacyBackupHttpError` factory) so subsequent subcommands stay DRY.
- Truncates API error response bodies to 1024 chars in tagged-error fields.
- Suppresses the fetching/restoring spinner in non-text output modes.
- Regenerates `@supabase/api` contracts from upstream OpenAPI (brings in the
  missing `id` field on backup items).
@Coly010 Coly010 requested a review from a team as a code owner May 21, 2026 12:58
@Coly010 Coly010 self-assigned this May 21, 2026
Coly010 added 7 commits May 21, 2026 14:31
…time

The bundled `supabase-legacy` binary panicked at runtime with
`Service not found: supabase/legacy/CliConfig` when running
`backups list` / `backups restore` (CI e2e parity shard).

`legacyProjectRefLayer` reads `LegacyCliConfig` directly for workdir
and projectId resolution. `Layer.provide` satisfies a target layer's
requirement but does not expose the provided service to siblings of a
`Layer.mergeAll(...)`, so providing `legacyCliConfigLayer` only to
`legacyBackupsPlatformApiLayer` is not enough.

Restore the explicit `Layer.provide(legacyCliConfigLayer)` on the
project-ref arm of `legacyBackupsRuntimeLayer` and document the
reasoning in a header comment so the trap isn't reintroduced.

Verified by building `dist/supabase-legacy` and invoking both commands;
they now reach the API and surface the expected 401 instead of the
service-resolution panic.
…ity)

The cli-e2e harness writes a per-test YAML profile to a temp path and
exports SUPABASE_PROFILE=<that-path> for both the Go and ts-legacy
binaries. Go's LoadProfile (apps/cli-go/internal/utils/profile.go)
implements dual semantics: built-in profile name first, YAML file path
second. The native TS port only handled the built-in branch, so
unknown tokens silently fell back to "supabase" — every API call hit
api.supabase.com instead of the local replay server and parity tests
failed with HTTP 401.

Mirror Go's dual semantics in `legacyCliConfigLayer`: if SUPABASE_PROFILE
isn't a built-in name, treat it as a YAML config-file path and read
`api_url` / `name` from it. Fall back to the `supabase` built-in if the
file is missing or malformed. Widen `LegacyCliConfig.profile` from the
string-literal union to `string` since the YAML `name:` is arbitrary
user input (the sole consumer reads it as a keyring account name).

Adds three unit tests covering the new branch, documents the dual
semantics in both backups SIDE_EFFECTS files, and updates the stale
"ts-legacy shells out to Go" comment in the cli-test-helpers harness.
@clack/prompts' log.error / log.message default to process.stdout. The
Go CLI writes failure messages to stderr, so any native-ported legacy
command's error output landed on the wrong stream — visible in
cli-e2e parity tests as "stderr differs" for backups list/restore
401/403/404/429/500/422 error paths, and as testBehaviour assertions
on result.stderr.toContain(...) failing because stderr was empty.

Pass `{ output: process.stderr }` to clack's log.error / log.message
calls in textOutputLayer.fail so the error block ("■ <msg>") and gray
detail line render on stderr. The outro suggestion stays on stdout
(matches clack's intro/outro convention; not load-bearing for parity).
The Go CLI wraps http.DefaultTransport with a stderr logger when --debug
is set (apps/cli-go/internal/debug/http.go), producing "HTTP <ts>
<METHOD>: <URL>" lines. cli-e2e asserts on this for `backups list
--debug` and the native legacy port had no equivalent.

Add `legacyHttpClientLayer` (apps/cli/src/legacy/auth/legacy-http-debug.layer.ts)
that conditionally wraps `FetchHttpClient.layer` with an `HttpClient.mapRequest`
middleware reading `LegacyDebugFlag`. When the flag is unset the layer
is identity over FetchHttpClient; when set, every outgoing request
prints `HTTP YYYY/MM/DD HH:MM:SS <METHOD>: <URL>\n` to stderr in Go's
exact `log.LstdFlags|log.Lmsgprefix` format.

Wire into backups.layers.ts in place of `FetchHttpClient.layer`.
Subsequent native ports composing through `legacyBackupsRuntimeLayer`
or copying its pattern inherit the same behavior.
Replaces clack's `log.error` framing (`│` guide + `■` icon) with raw
process.stderr.write that mirrors Go's `recoverAndExit`
(apps/cli-go/cmd/root.go:300-303): a single red-styled message line
followed by an optional suggestion. When the caller doesn't provide
its own suggestion, fall back to Go's `SuggestDebugFlag` string
("Try rerunning the command with --debug to troubleshoot the error.")
— unless `--debug` is already set, matching the Go gate.

Also: the Go binary prints github.com/go-errors/errors stack frames
before the message in dev builds (`utils.Version == ""`). cli-e2e
exercises these dev builds, so even after byte-matching the message the
parity comparison would diff on the leading stack-trace block. The TS
port intentionally doesn't reconstruct these frames; add a normalize
rule in packages/cli-test-helpers/src/normalize.ts to strip the Go
frame block (matched after rules 8 and 10 normalize the path and
address to literals) plus the trailing blank line.

Updates the existing `output.layer.unit.test.ts` `fail` test to assert
on raw stderr bytes; adds two cases for the --debug suggestion
fallback (present when --debug unset, omitted when set).
Mirrors Go's ensureProjectGroupsCached (apps/cli-go/cmd/root.go:213-234):
when --project-ref is set and supabase/.temp/linked-project.json doesn't
exist yet, fetch GET /v1/projects/<ref> and write the project metadata to
the cache.

Adds:
- LegacyLinkedProjectCache service + layer at
  apps/cli/src/legacy/telemetry/. Best-effort: auth, network, schema,
  and filesystem errors are all swallowed (matches Go's "log to debug
  and return" behavior).
- Uses HttpClient directly rather than the typed LegacyPlatformApi
  client. The generated V1ProjectWithDatabaseResponse schema enforces a
  20-char project-ref length that cli-e2e replay fixtures (which store
  `__PROJECT_REF__` placeholder strings) cannot satisfy. The cache only
  reads four string fields and doesn't validate them.
- Body shape matches LinkedProject from
  apps/cli-go/internal/telemetry/project.go:15-20.
- Hook from each backups handler via Effect.ensuring(cache.cache(ref))
  so the cache fires whether the main API call succeeds or fails
  (matches Go's PersistentPostRun).

Updates integration test setups to provide a no-op
mockLinkedProjectCacheLayer so the new service requirement doesn't break
the existing handler tests.
Mirrors Go's LoadOrCreateState (apps/cli-go/internal/telemetry/state.go:74-98):
on every command invocation, write a JSON telemetry-state file with a
persistent device_id, a session_id that rotates after 30 minutes of
inactivity, the current timestamp, and a schema_version. The file lives
at $SUPABASE_HOME/telemetry.json (or ~/.supabase/telemetry.json), matching
Go's telemetryPath() exactly.

Adds:
- LegacyTelemetryState service + layer at
  apps/cli/src/legacy/telemetry/. Best-effort: filesystem and JSON-parse
  errors are swallowed.
- Field order matches Go's struct declaration: enabled, device_id,
  session_id, session_last_active, distinct_id?, schema_version. The
  enabled flag stays true on fresh creation; only the user's
  `supabase telemetry disable` flips it. `SUPABASE_TELEMETRY_DISABLED`
  / `DO_NOT_TRACK` env vars suppress event delivery, not file writes
  (matches Go).
- Hooks from each backups handler via Effect.ensuring(flush) — fires
  whether the main API call succeeds or fails (matches Go's
  PersistentPostRun).

Updates integration test setups to provide a no-op
mockTelemetryStateLayer.

Completes the Go-parity infrastructure backups list/restore needed for
cli-e2e parity tests to pass: filesystem snapshots now match Go's
output byte-for-byte after normalize().
Copy link
Copy Markdown
Contributor

@jgoux jgoux left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a remark about e2e tests, solid work! 👍

Don't hesitate to add more instructions in AGENTS.md if you think they are helping the agent nailing the port (and we can all benefit from it later if we help!)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if these e2e tests are useful. The golden e2e test for this command would be to actually hit the backend and assert we see the list of backups.
I hope we can run Supabase as a platform for this kind of test soon in CI. 😄
But for now I would simply don't have an e2e test at all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants