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
3 changes: 3 additions & 0 deletions apps/cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ src/legacy/commands/<command>/
<command>.layers.ts # runtime layer composition for the command family
<command>.format.ts # text formatters (timestamps, regions, booleans)
<command>.encoders.ts # Go-compatible JSON / YAML / TOML / env encoders
<command>.go-payload.ts # Go struct specs mirroring types.gen.go — drive `-o yaml|toml` key casing (CLI-1975)
SIDE_EFFECTS.md
```

Expand Down Expand Up @@ -275,6 +276,8 @@ When porting a Management-API-style command, verify each item before marking the

7. **PostHog telemetry payload matches Go 1:1** — see the next section.

8. **Go API type regen re-syncs `*.go-payload.ts` specs** — when `apps/cli-go/pkg/api/types.gen.go` regenerates, re-audit every `*.go-payload.ts`/inline `LegacyGoType` struct spec that mirrors it (field order, JSON/Go name pairs); nothing checks this mechanically today (CLI-1975, review kanadgupta).

---

## Legacy Port: Telemetry Parity
Expand Down
11 changes: 9 additions & 2 deletions apps/cli/src/legacy/commands/backups/list/SIDE_EFFECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,18 @@ Indented JSON (`json.MarshalIndent(resp, "", " ")` equivalent) of the full back

### `--output yaml`

YAML document (`yaml@2` equivalent of Go's `yaml.v3`) of the full backup response.
YAML document matching Go's `yaml.v3` output byte-for-byte (CLI-1975): keys are
the lowercased Go struct field names (`walgenabled`, `physicalbackupdata`), nil
pointers render as explicit `null`, and nested mappings use yaml.v3's 4-column
indentation.

### `--output toml`

TOML document (`smol-toml` equivalent of Go's `BurntSushi/toml`) of the full backup response. JSON shape is preserved; leaf order may differ from Go.
TOML document matching Go's `BurntSushi/toml` output byte-for-byte (CLI-1975):
keys are the PascalCase Go struct field names (`WalgEnabled`,
`[PhysicalBackupData]`), nil pointer fields are omitted, and sub-tables follow
the primitive keys with 2-space indentation. An empty `backups` array is
treated as Go's nil slice (omitted).

### `--output env`

Expand Down
55 changes: 48 additions & 7 deletions apps/cli/src/legacy/commands/backups/list/list.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,47 @@ import {
LegacyBackupListNetworkError,
LegacyBackupListUnexpectedStatusError,
} from "../backups.errors.ts";
import { encodeEnv, encodeGoJson } from "../../../shared/legacy-go-output.encoders.ts";
import {
encodeEnv,
encodeGoJson,
encodeToml,
encodeYaml,
} from "../../../shared/legacy-go-output.encoders.ts";
encodeLegacyGoToml,
encodeLegacyGoYaml,
legacyGoBool,
legacyGoInt,
legacyGoPtr,
legacyGoSlice,
legacyGoString,
legacyGoStruct,
} from "../../../shared/legacy-go-struct-output.encoders.ts";
import { mapLegacyHttpError } from "../../../shared/legacy-http-errors.ts";
import { formatLegacyTimestamp } from "../../../shared/legacy-timestamp.format.ts";
import { formatRegion } from "../backups.format.ts";
import type { LegacyBackupsListFlags } from "./list.command.ts";

/** Mirror of Go's `api.V1BackupsResponse` (`apps/cli-go/pkg/api/types.gen.go`). */
const LEGACY_GO_BACKUPS_RESPONSE = legacyGoStruct([
[
"backups",
legacyGoSlice(
legacyGoStruct([
["id", legacyGoInt],
["inserted_at", legacyGoString],
["is_physical_backup", legacyGoBool],
["status", legacyGoString],
]),
),
],
[
"physical_backup_data",
legacyGoStruct([
["earliest_physical_backup_date_unix", legacyGoPtr(legacyGoInt)],
["latest_physical_backup_date_unix", legacyGoPtr(legacyGoInt)],
]),
],
["pitr_enabled", legacyGoBool],
["region", legacyGoString],
["walg_enabled", legacyGoBool],
]);

type BackupsResponse = typeof V1ListAllBackupsOutput.Type;

const mapListError = mapLegacyHttpError({
Expand Down Expand Up @@ -95,11 +125,22 @@ export const legacyBackupsList = Effect.fn("legacy.backups.list")(function* (
return;
}
if (goFmt === "yaml") {
yield* output.raw(encodeYaml(response));
yield* output.raw(encodeLegacyGoYaml(response, LEGACY_GO_BACKUPS_RESPONSE));
return;
}
if (goFmt === "toml") {
yield* output.raw(encodeToml(response) + "\n");
// The schema decodes Go's PITR-only `"backups": null` to `[]` (see the
// `nullForEmptyArrays` JSON hint above); mirror that by treating an
// empty list as Go's nil slice, which BurntSushi omits entirely.
yield* output.raw(
encodeLegacyGoToml(
{
...response,
backups: response.backups.length > 0 ? response.backups : undefined,
Comment thread
Coly010 marked this conversation as resolved.
},
LEGACY_GO_BACKUPS_RESPONSE,
),
);
return;
}
if (goFmt === "env") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,16 +165,39 @@ describe("legacy backups list integration", () => {
return Effect.gen(function* () {
yield* legacyBackupsList({ projectRef: Option.none() });
expect(out.stdoutText).toContain("region: ap-southeast-1");
expect(out.stdoutText).toContain("walg_enabled: true");
// yaml.v3 lowercases the whole Go field name (CLI-1975).
expect(out.stdoutText).toContain("walgenabled: true");
}).pipe(Effect.provide(layer));
});

it.live("emits TOML to stdout for --output toml", () => {
const { layer, out } = setup({ goOutput: "toml", response: PITR_RESPONSE });
return Effect.gen(function* () {
yield* legacyBackupsList({ projectRef: Option.none() });
expect(out.stdoutText).toContain('region = "ap-southeast-1"');
expect(out.stdoutText).toContain("walg_enabled = true");
// BurntSushi emits PascalCase Go field names (CLI-1975).
expect(out.stdoutText).toContain('Region = "ap-southeast-1"');
expect(out.stdoutText).toContain("WalgEnabled = true");
}).pipe(Effect.provide(layer));
});

it.live("emits [[Backups]] array-of-tables for --output toml with logical backups", () => {
const { layer, out } = setup({ goOutput: "toml", response: LOGICAL_RESPONSE });
return Effect.gen(function* () {
yield* legacyBackupsList({ projectRef: Option.none() });
// Byte-exact Go parity (CLI-1975): primitives first, then the Backups
// array-of-tables and the (empty) PhysicalBackupData table.
expect(out.stdoutText).toBe(`PitrEnabled = true
Region = "ap-southeast-1"
WalgEnabled = true

[[Backups]]
Id = 1
InsertedAt = "2026-02-08T16:44:07Z"
IsPhysicalBackup = true
Status = "COMPLETED"

[PhysicalBackupData]
`);
}).pipe(Effect.provide(layer));
});

Expand Down
51 changes: 51 additions & 0 deletions apps/cli/src/legacy/commands/branches/branches.go-payload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {
type LegacyGoType,
legacyGoBool,
legacyGoFloat32,
legacyGoInt,
legacyGoPtr,
legacyGoSlice,
legacyGoString,
legacyGoStruct,
legacyGoTime,
legacyGoTomlListWrapper,
legacyGoUuid,
} from "../../shared/legacy-go-struct-output.encoders.ts";

/**
* Mirror of Go's `api.BranchResponse` (`apps/cli-go/pkg/api/types.gen.go`) —
* field order and pointer-ness drive the `-o yaml` / `-o toml` byte shape
* (CLI-1975). Shared by `branches list`, `branches create`, and
* `branches update`, which all encode this struct.
*/
export const LEGACY_GO_BRANCH_RESPONSE: LegacyGoType = legacyGoStruct([
Comment thread
Coly010 marked this conversation as resolved.
["created_at", legacyGoTime],
["deletion_scheduled_at", legacyGoPtr(legacyGoTime)],
["git_branch", legacyGoPtr(legacyGoString)],
["id", legacyGoUuid],
["is_default", legacyGoBool],
["latest_check_run_id", legacyGoPtr(legacyGoFloat32)],
["name", legacyGoString],
["notify_url", legacyGoPtr(legacyGoString)],
["parent_project_ref", legacyGoString],
["persistent", legacyGoBool],
["pr_number", legacyGoPtr(legacyGoInt)],
["preview_project_status", legacyGoPtr(legacyGoString)],
["project_ref", legacyGoString],
["review_requested_at", legacyGoPtr(legacyGoTime)],
["status", legacyGoString],
["updated_at", legacyGoTime],
["with_data", legacyGoBool],
]);

/** `branches list -o yaml` encodes the bare `[]api.BranchResponse`. */
export const LEGACY_GO_BRANCHES_LIST: LegacyGoType = legacyGoSlice(LEGACY_GO_BRANCH_RESPONSE);

/**
* `branches list -o toml` wraps the slice:
* `struct{ Branches []api.BranchResponse `toml:"branches"` }`.
*/
export const LEGACY_GO_BRANCHES_TOML_WRAPPER: LegacyGoType = legacyGoTomlListWrapper(
"branches",
LEGACY_GO_BRANCH_RESPONSE,
);
14 changes: 7 additions & 7 deletions apps/cli/src/legacy/commands/branches/create/create.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import { CONTEXT_CANCELED_MESSAGE } from "../../../../shared/output/errors.ts";
import { Output } from "../../../../shared/output/output.service.ts";
import { detectGitBranch } from "../../../../shared/git/git-branch.ts";
import { legacyAqua } from "../../../shared/legacy-colors.ts";
import { encodeEnv, encodeGoJson } from "../../../shared/legacy-go-output.encoders.ts";
import {
encodeEnv,
encodeGoJson,
encodeToml,
encodeYaml,
} from "../../../shared/legacy-go-output.encoders.ts";
encodeLegacyGoToml,
encodeLegacyGoYaml,
} from "../../../shared/legacy-go-struct-output.encoders.ts";
import { mapLegacyHttpError } from "../../../shared/legacy-http-errors.ts";
import { legacyGateMapError } from "../../../shared/legacy-upgrade-suggest.ts";
import { LEGACY_GO_BRANCH_RESPONSE } from "../branches.go-payload.ts";
import {
LegacyBranchesCreateCancelledError,
LegacyBranchesCreateNetworkError,
Expand Down Expand Up @@ -123,12 +123,12 @@ export const legacyBranchesCreate = Effect.fn("legacy.branches.create")(function
}
if (goFmt === "yaml") {
yield* output.raw("Created preview branch:\n");
yield* output.raw(encodeYaml(created));
yield* output.raw(encodeLegacyGoYaml(created, LEGACY_GO_BRANCH_RESPONSE));
return;
}
if (goFmt === "toml") {
yield* output.raw("Created preview branch:\n");
yield* output.raw(encodeToml(created) + "\n");
yield* output.raw(encodeLegacyGoToml(created, LEGACY_GO_BRANCH_RESPONSE));
return;
}
if (goFmt === "env") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,20 @@ describe("legacy branches get integration", () => {
}).pipe(Effect.provide(layer));
});

it.live(
"keeps env-map keys verbatim for --output toml (map payload, exempt from CLI-1975)",
() => {
const { layer, out } = setup({ goOutput: "toml" });
return Effect.gen(function* () {
yield* legacyBranchesGet({ ...baseFlags, name: Option.some(BRANCH_UUID) });
// Go encodes a map[string]string here — BurntSushi keeps map keys as-is
// (no PascalCase remap), so the CLI-1975 struct remap must NOT apply.
expect(out.stdoutText).toContain('SUPABASE_URL = "');
expect(out.stdoutText).toContain('POSTGRES_URL = "');
}).pipe(Effect.provide(layer));
},
);

it.live("emits standard-env map for --output env (env-format encoder)", () => {
const { layer, out } = setup({ goOutput: "env" });
return Effect.gen(function* () {
Expand Down
21 changes: 18 additions & 3 deletions apps/cli/src/legacy/commands/branches/list/list.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,16 @@ import { LegacyLinkedProjectCache } from "../../../telemetry/legacy-linked-proje
import { LegacyTelemetryState } from "../../../telemetry/legacy-telemetry-state.service.ts";
import { LegacyOutputFlag } from "../../../../shared/legacy/global-flags.ts";
import { Output } from "../../../../shared/output/output.service.ts";
import { encodeGoJson, encodeToml, encodeYaml } from "../../../shared/legacy-go-output.encoders.ts";
import { encodeGoJson } from "../../../shared/legacy-go-output.encoders.ts";
import {
encodeLegacyGoToml,
encodeLegacyGoYaml,
} from "../../../shared/legacy-go-struct-output.encoders.ts";
import { mapLegacyHttpError } from "../../../shared/legacy-http-errors.ts";
import {
LEGACY_GO_BRANCHES_LIST,
LEGACY_GO_BRANCHES_TOML_WRAPPER,
} from "../branches.go-payload.ts";
import {
LegacyBranchesEnvNotSupportedError,
LegacyBranchesListNetworkError,
Expand Down Expand Up @@ -59,11 +67,18 @@ export const legacyBranchesList = Effect.fn("legacy.branches.list")(function* (
return;
}
if (goFmt === "yaml") {
yield* output.raw(encodeYaml(branches));
yield* output.raw(encodeLegacyGoYaml(branches, LEGACY_GO_BRANCHES_LIST));
return;
}
if (goFmt === "toml") {
yield* output.raw(encodeToml({ branches }) + "\n");
// Go builds the list with `append` (`list.go:70-80`), so an empty list
// stays a nil slice and BurntSushi emits nothing for the wrapper.
yield* output.raw(
encodeLegacyGoToml(
{ branches: branches.length > 0 ? branches : undefined },
LEGACY_GO_BRANCHES_TOML_WRAPPER,
),
);
return;
}

Expand Down
Loading
Loading