diff --git a/apps/cli/src/command-internal/db-pull-run.ts b/apps/cli/src/command-internal/db-pull-run.ts index d01b9d1c54..1f92838e7c 100644 --- a/apps/cli/src/command-internal/db-pull-run.ts +++ b/apps/cli/src/command-internal/db-pull-run.ts @@ -90,7 +90,7 @@ export type { DbPullFlags }; // Established output contract; ends with a `.`. const DEPRECATION_LINE = - "Flag --use-pg-delta has been deprecated, use --declarative with [experimental.pgdelta] enabled = true in your config.toml instead."; + "Flag --use-pg-delta is deprecated and will be removed in v3, use --declarative with [experimental.pgdelta] enabled = true in your config.toml instead."; /** * Explains the in-sync non-zero exit instead of the generic "Try rerunning the command with @@ -107,7 +107,7 @@ const MIGRATION_FILE_MODE = 0o644; // export covers the same per-object-files outcome. Printed only when the experimental gate // selected this branch, not when `--declarative` already did. const EXPERIMENTAL_STRUCTURED_DUMP_DEPRECATION_LINE = - "The --experimental structured-dump mode for `db pull` is deprecated and will be removed in a future release. Use --declarative instead to pull the remote schema as per-object files."; + "The --experimental structured-dump mode for `db pull` is deprecated and will be removed in v3. Use --declarative instead to pull the remote schema as per-object files."; export type DbPullInvoke = { /** Skip printing `Finished supabase db pull.` for callers that must not emit it. */ diff --git a/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md b/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md index dd5da5ad77..81db74a60e 100644 --- a/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md @@ -163,7 +163,7 @@ Progress to stderr. Migration path: `Creating shadow database...`, `Diffing schemas[: ]`, `Schema written to `. Declarative path: `Preparing declarative schema export using pg-delta...`, `Declarative schema written to `. Plus the `--use-pg-delta` deprecation line, the -`--experimental` structured-dump deprecation line, and the history-update +`--experimental` structured-dump deprecation line (both name v3), and the history-update prompt. On success the PostRun line `Finished supabase db pull.` is printed to stdout. @@ -190,7 +190,7 @@ Progress strings still go to stderr; stdout carries a single structured envelope env var simply leaves unused on a non-linked target). The deprecated `--experimental` export honors `--project-ref` the same way `--declarative` does. -- `--use-pg-delta` is hidden and emits the cobra deprecation line to stderr. +- `--use-pg-delta` is hidden and emits `Flag --use-pg-delta is deprecated and will be removed in v3, use --declarative with [experimental.pgdelta] enabled = true in your config.toml instead.` to stderr. - Migration-style pulls always compare migrations with the live target; declarative files and `schema_paths` do not replace that baseline. - Bundled nontransactional files begin with @@ -201,8 +201,8 @@ Progress strings still go to stderr; stdout carries a single structured envelope empty diff is "No schema changes found". Stack-backed pulls never take this path. - The `--experimental` structured-dump branch (or the `SUPABASE_EXPERIMENTAL` project-`.env` equivalent) now runs the same in-process declarative export - as `--declarative`. It is deprecated: a warning pointing at `--declarative` - prints to stderr before the export. `--experimental --declarative` does not + as `--declarative`. It is deprecated and will be removed in v3: a warning pointing + at `--declarative` prints to stderr before the export. `--experimental --declarative` does not print that line. Output is the pg-delta declarative tree under `supabase/schemas` (plus `.pgdelta-export.json`), not Go's former `schemas/` + `cluster/` AST split. diff --git a/apps/cli/src/commands/db/pull/pull.integration.test.ts b/apps/cli/src/commands/db/pull/pull.integration.test.ts index a626a31c54..b0be6b3e14 100644 --- a/apps/cli/src/commands/db/pull/pull.integration.test.ts +++ b/apps/cli/src/commands/db/pull/pull.integration.test.ts @@ -1040,7 +1040,9 @@ describe("db pull", () => { const s = setup(tmp.current, { edgeStdout: EXPORT_JSON }); return Effect.gen(function* () { yield* dbPull(flags({ usePgDelta: Option.some(true) })); - expect(streamText(s.out, "stderr")).toContain("Flag --use-pg-delta has been deprecated"); + expect(streamText(s.out, "stderr")).toContain( + "Flag --use-pg-delta is deprecated and will be removed in v3, use --declarative with [experimental.pgdelta] enabled = true in your config.toml instead.\n", + ); const path = yield* Path.Path; expect(streamText(s.out, "stderr")).toContain( `Declarative schema written to ${path.join("supabase", "schemas")}\n`, @@ -1763,7 +1765,7 @@ describe("db pull", () => { expect(s.engineCalls[0]?.operation).toBe("export"); expect(streamText(s.out, "stderr")).toContain("Connecting to remote database..."); expect(streamText(s.out, "stderr")).toContain( - "The --experimental structured-dump mode for `db pull` is deprecated", + "The --experimental structured-dump mode for `db pull` is deprecated and will be removed in v3.", ); expect(streamText(s.out, "stderr")).toContain("Preparing declarative schema export"); }).pipe(Effect.provide(s.layer)); @@ -1818,7 +1820,7 @@ describe("db pull", () => { expect(s.engineCalls[0]?.operation).toBe("export"); expect(s.proxyCalls).toHaveLength(0); expect(streamText(s.out, "stderr")).toContain( - "The --experimental structured-dump mode for `db pull` is deprecated", + "The --experimental structured-dump mode for `db pull` is deprecated and will be removed in v3.", ); }).pipe(Effect.provide(s.layer)); }); @@ -1842,7 +1844,7 @@ describe("db pull", () => { expect(s.engineCalls[0]?.operation).toBe("export"); expect(streamText(s.out, "stderr")).toContain("Connecting to remote database..."); expect(streamText(s.out, "stderr")).toContain( - "The --experimental structured-dump mode for `db pull` is deprecated", + "The --experimental structured-dump mode for `db pull` is deprecated and will be removed in v3.", ); }).pipe(Effect.provide(s.layer)); }, @@ -2412,7 +2414,9 @@ describe("db remote commit", () => { expect(err).toContain( `Command "commit" is deprecated and will be removed in v3, use "db pull" instead.\n`, ); - expect(err).toContain("The --experimental structured-dump mode for `db pull` is deprecated"); + expect(err).toContain( + "The --experimental structured-dump mode for `db pull` is deprecated and will be removed in v3.", + ); const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; expect( diff --git a/apps/cli/src/commands/domains/SIDE_EFFECTS.md b/apps/cli/src/commands/domains/SIDE_EFFECTS.md index ed8baa4a33..9c2e7404f9 100644 --- a/apps/cli/src/commands/domains/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/domains/SIDE_EFFECTS.md @@ -93,8 +93,9 @@ suppressed on stderr. `delete` ignores `-o`. - `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` → `SUPABASE_PROJECT_ID` env → linked-project file. - The project-ref fallback env var is `SUPABASE_PROJECT_ID`. +- `--include-raw-output` is deprecated: passing it with any value (including `--include-raw-output=false`) prints `Flag --include-raw-output is deprecated and will be removed in v3, use -o json instead.` to stderr before the API call, in every output mode, on all five subcommands (including `delete`). stdout is unaffected. - **Documented divergences from the old Go CLI (intentional):** - - `--include-raw-output` is declared as a normal boolean **on each subcommand** (the old Go CLI declared 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 the old 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. + - `--include-raw-output` is declared as an optional boolean **on each subcommand** (the old Go CLI declared 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 the old help-hiding. It still reproduces the behavioral effect (forces `-o json` when the value is `true` and `-o` is unset/pretty); on `delete` it is inert. - The degenerate `validation_records != 1` status message approximates a Go-style `%+v` struct dump (which embeds a non-deterministic pointer address). - Text-mode status output is newline-terminated even where the old CLI's output wasn't. Without the final newline, interactive shell prompts can redraw over the last status line, hiding the ACME TXT record. - In a structured `-o` mode the human status is suppressed on stderr, keeping stdout clean and matching the parity contract. diff --git a/apps/cli/src/commands/domains/activate/activate.command.ts b/apps/cli/src/commands/domains/activate/activate.command.ts index e4cb5d7b8a..9a83478686 100644 --- a/apps/cli/src/commands/domains/activate/activate.command.ts +++ b/apps/cli/src/commands/domains/activate/activate.command.ts @@ -13,7 +13,7 @@ const config = { ), includeRawOutput: Flag.Boolean("include-raw-output").pipe( Flag.withDescription("(Deprecated) use -o json instead."), - Flag.withDefault(false), + Flag.optional, ), } as const; diff --git a/apps/cli/src/commands/domains/activate/activate.handler.ts b/apps/cli/src/commands/domains/activate/activate.handler.ts index cc267ad126..e3844d3436 100644 --- a/apps/cli/src/commands/domains/activate/activate.handler.ts +++ b/apps/cli/src/commands/domains/activate/activate.handler.ts @@ -1,10 +1,11 @@ -import { Effect } from "effect"; +import { Effect, Option } from "effect"; import { CommandPlatformApi } from "../../../auth/command-platform-api.service.ts"; import { ProjectRefResolver } from "../../../config/project-ref.service.ts"; import { Output } from "../../../shared/output/output.service.ts"; import { LinkedProjectCache } from "../../../telemetry/linked-project-cache.service.ts"; import { TelemetryState } from "../../../telemetry/telemetry-state.service.ts"; +import { warnIncludeRawOutputDeprecated } from "../domains.deprecation.ts"; import { emitHostnameResult } from "../domains.emit.ts"; import { mapDomainsHttpError } from "../domains.errors.ts"; import { gateMapError } from "../../../command-internal/upgrade-suggest.ts"; @@ -21,6 +22,8 @@ export const domainsActivate = Effect.fn("domains.activate")(function* ( const linkedProjectCache = yield* LinkedProjectCache; const telemetryState = yield* TelemetryState; + yield* warnIncludeRawOutputDeprecated(flags.includeRawOutput); + const ref = yield* resolver.resolve(flags.projectRef); yield* Effect.gen(function* () { @@ -32,6 +35,9 @@ export const domainsActivate = Effect.fn("domains.activate")(function* ( ); yield* activating?.clear ?? Effect.void; - yield* emitHostnameResult(response, flags.includeRawOutput); + yield* emitHostnameResult( + response, + Option.getOrElse(flags.includeRawOutput, () => false), + ); }).pipe(Effect.ensuring(linkedProjectCache.cache(ref)), Effect.ensuring(telemetryState.flush)); }); diff --git a/apps/cli/src/commands/domains/activate/activate.integration.test.ts b/apps/cli/src/commands/domains/activate/activate.integration.test.ts index eabbff260a..f352793246 100644 --- a/apps/cli/src/commands/domains/activate/activate.integration.test.ts +++ b/apps/cli/src/commands/domains/activate/activate.integration.test.ts @@ -1,6 +1,6 @@ import { type V1GetHostnameConfigOutput } from "@supabase/api/effect"; import { describe, expect, it } from "@effect/vitest"; -import { Cause, Effect, Exit, Option } from "effect"; +import { Cause, Effect, Exit, Option, Schema } from "effect"; import { mockAnalytics, mockOutput } from "../../../../tests/helpers/mocks.ts"; import { @@ -65,7 +65,9 @@ function setup(opts: SetupOpts = {}) { return { layer, out, api, analytics, telemetry, linkedProjectCache }; } -const baseFlags = { projectRef: Option.none(), includeRawOutput: false }; +const baseFlags = { projectRef: Option.none(), includeRawOutput: Option.none() }; + +const DeprecationJsonObject = Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown)); describe("domains activate integration", () => { it.effect("suggests upgrade from entitlement_required envelope on 400", () => { @@ -127,11 +129,39 @@ describe("domains activate integration", () => { }).pipe(Effect.provide(layer)); }); - it.effect("forces Go JSON output when --include-raw-output is set", () => { + it.effect( + "warns on stderr and keeps stdout JSON parseable when --include-raw-output is set", + () => { + const { layer, out } = setup(); + return Effect.gen(function* () { + yield* domainsActivate({ projectRef: Option.none(), includeRawOutput: Option.some(true) }); + expect(out.stderrText).toBe( + "Flag --include-raw-output is deprecated and will be removed in v3, use -o json instead.\n", + ); + const parsed = yield* Schema.decodeEffect(DeprecationJsonObject)(out.stdoutText); + expect(parsed.custom_hostname).toBe("shop.acme.dev"); + }).pipe(Effect.provide(layer)); + }, + ); + + it.effect("warns but does not force JSON when --include-raw-output=false", () => { const { layer, out } = setup(); return Effect.gen(function* () { - yield* domainsActivate({ projectRef: Option.none(), includeRawOutput: true }); - expect(out.stdoutText.startsWith("{")).toBe(true); + yield* domainsActivate({ projectRef: Option.none(), includeRawOutput: Option.some(false) }); + expect( + out.stderrText.startsWith( + "Flag --include-raw-output is deprecated and will be removed in v3, use -o json instead.\n", + ), + ).toBe(true); + expect(out.stdoutText).toBe(""); + }).pipe(Effect.provide(layer)); + }); + + it.effect("does not warn when --include-raw-output is not set", () => { + const { layer, out } = setup(); + return Effect.gen(function* () { + yield* domainsActivate(baseFlags); + expect(out.stderrText).not.toContain("deprecated"); }).pipe(Effect.provide(layer)); }); diff --git a/apps/cli/src/commands/domains/create/create.command.ts b/apps/cli/src/commands/domains/create/create.command.ts index 008fde822d..f8f7737ed6 100644 --- a/apps/cli/src/commands/domains/create/create.command.ts +++ b/apps/cli/src/commands/domains/create/create.command.ts @@ -16,7 +16,7 @@ const config = { ), includeRawOutput: Flag.Boolean("include-raw-output").pipe( Flag.withDescription("(Deprecated) use -o json instead."), - Flag.withDefault(false), + Flag.optional, ), } as const; diff --git a/apps/cli/src/commands/domains/create/create.handler.ts b/apps/cli/src/commands/domains/create/create.handler.ts index 96d91eca98..76336a82aa 100644 --- a/apps/cli/src/commands/domains/create/create.handler.ts +++ b/apps/cli/src/commands/domains/create/create.handler.ts @@ -1,4 +1,4 @@ -import { Effect } from "effect"; +import { Effect, Option } from "effect"; import * as HttpClient from "effect/http/HttpClient"; import { CommandPlatformApi } from "../../../auth/command-platform-api.service.ts"; @@ -8,6 +8,7 @@ import { Output } from "../../../shared/output/output.service.ts"; import { LinkedProjectCache } from "../../../telemetry/linked-project-cache.service.ts"; import { TelemetryState } from "../../../telemetry/telemetry-state.service.ts"; import { verifyCname } from "../domains.cname.ts"; +import { warnIncludeRawOutputDeprecated } from "../domains.deprecation.ts"; import { emitHostnameResult } from "../domains.emit.ts"; import { mapDomainsHttpError } from "../domains.errors.ts"; import { gateMapError } from "../../../command-internal/upgrade-suggest.ts"; @@ -24,6 +25,8 @@ export const domainsCreate = Effect.fn("domains.create")(function* (flags: Domai const linkedProjectCache = yield* LinkedProjectCache; const telemetryState = yield* TelemetryState; + yield* warnIncludeRawOutputDeprecated(flags.includeRawOutput); + const ref = yield* resolver.resolve(flags.projectRef); // Write the linked-project cache and persist the telemetry state file on @@ -48,6 +51,9 @@ export const domainsCreate = Effect.fn("domains.create")(function* (flags: Domai ); yield* creating?.clear ?? Effect.void; - yield* emitHostnameResult(response, flags.includeRawOutput); + yield* emitHostnameResult( + response, + Option.getOrElse(flags.includeRawOutput, () => false), + ); }).pipe(Effect.ensuring(linkedProjectCache.cache(ref)), Effect.ensuring(telemetryState.flush)); }); diff --git a/apps/cli/src/commands/domains/create/create.integration.test.ts b/apps/cli/src/commands/domains/create/create.integration.test.ts index 07571e7642..6ccb0b2d23 100644 --- a/apps/cli/src/commands/domains/create/create.integration.test.ts +++ b/apps/cli/src/commands/domains/create/create.integration.test.ts @@ -1,6 +1,6 @@ import { type V1GetHostnameConfigOutput } from "@supabase/api/effect"; import { describe, expect, it } from "@effect/vitest"; -import { Cause, Deferred, Effect, Exit, Fiber, Option } from "effect"; +import { Cause, Deferred, Effect, Exit, Fiber, Option, Schema } from "effect"; import { TestClock } from "effect/testing"; import { mockAnalytics, mockOutput } from "../../../../tests/helpers/mocks.ts"; @@ -98,11 +98,11 @@ function setup(opts: SetupOpts = {}) { return { layer, out, api, analytics, telemetry, linkedProjectCache }; } -function flags(over: Partial<{ includeRawOutput: boolean }> = {}) { +function flags(over: Partial<{ includeRawOutput: Option.Option }> = {}) { return { projectRef: Option.none(), customHostname: CUSTOM_HOSTNAME, - includeRawOutput: over.includeRawOutput ?? false, + includeRawOutput: over.includeRawOutput ?? Option.none(), }; } @@ -110,6 +110,8 @@ function postedToInitialize(api: { requests: ReadonlyArray<{ url: string }> }): return api.requests.some((r) => r.url.includes("/custom-hostname/initialize")); } +const DeprecationJsonObject = Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown)); + describe("domains create integration", () => { it.effect("verifies the CNAME, creates the hostname, and prints status to stderr", () => { const { layer, out, api, telemetry, linkedProjectCache } = setup(); @@ -262,11 +264,39 @@ describe("domains create integration", () => { }).pipe(Effect.provide(layer)); }); - it.effect("forces Go JSON output when --include-raw-output is set", () => { + it.effect( + "warns on stderr and keeps stdout JSON parseable when --include-raw-output is set", + () => { + const { layer, out } = setup(); + return Effect.gen(function* () { + yield* domainsCreate(flags({ includeRawOutput: Option.some(true) })); + expect(out.stderrText).toBe( + "Flag --include-raw-output is deprecated and will be removed in v3, use -o json instead.\n", + ); + const parsed = yield* Schema.decodeEffect(DeprecationJsonObject)(out.stdoutText); + expect(parsed.custom_hostname).toBe(CUSTOM_HOSTNAME); + }).pipe(Effect.provide(layer)); + }, + ); + + it.effect("warns but does not force JSON when --include-raw-output=false", () => { const { layer, out } = setup(); return Effect.gen(function* () { - yield* domainsCreate(flags({ includeRawOutput: true })); - expect(out.stdoutText.startsWith("{")).toBe(true); + yield* domainsCreate(flags({ includeRawOutput: Option.some(false) })); + expect( + out.stderrText.startsWith( + "Flag --include-raw-output is deprecated and will be removed in v3, use -o json instead.\n", + ), + ).toBe(true); + expect(out.stdoutText).toBe(""); + }).pipe(Effect.provide(layer)); + }); + + it.effect("does not warn when --include-raw-output is not set", () => { + const { layer, out } = setup(); + return Effect.gen(function* () { + yield* domainsCreate(flags()); + expect(out.stderrText).not.toContain("deprecated"); }).pipe(Effect.provide(layer)); }); diff --git a/apps/cli/src/commands/domains/delete/delete.command.ts b/apps/cli/src/commands/domains/delete/delete.command.ts index 0206785052..c45cecd071 100644 --- a/apps/cli/src/commands/domains/delete/delete.command.ts +++ b/apps/cli/src/commands/domains/delete/delete.command.ts @@ -13,7 +13,7 @@ const config = { ), includeRawOutput: Flag.Boolean("include-raw-output").pipe( Flag.withDescription("(Deprecated) use -o json instead."), - Flag.withDefault(false), + Flag.optional, ), } as const; diff --git a/apps/cli/src/commands/domains/delete/delete.handler.ts b/apps/cli/src/commands/domains/delete/delete.handler.ts index 0cab96bb18..c8286c3017 100644 --- a/apps/cli/src/commands/domains/delete/delete.handler.ts +++ b/apps/cli/src/commands/domains/delete/delete.handler.ts @@ -5,6 +5,7 @@ import { ProjectRefResolver } from "../../../config/project-ref.service.ts"; import { Output } from "../../../shared/output/output.service.ts"; import { LinkedProjectCache } from "../../../telemetry/linked-project-cache.service.ts"; import { TelemetryState } from "../../../telemetry/telemetry-state.service.ts"; +import { warnIncludeRawOutputDeprecated } from "../domains.deprecation.ts"; import { mapDomainsHttpError } from "../domains.errors.ts"; import type { DomainsDeleteFlags } from "./delete.command.ts"; @@ -12,9 +13,8 @@ const mapDeleteError = mapDomainsHttpError("delete"); const DELETE_SUCCESS_MESSAGE = "Deleted custom hostname config successfully."; -// `flags.includeRawOutput` is unread: `--include-raw-output` is a persistent flag on the -// `domains` group, so it's accepted on `delete` too, but ignored — delete has no response -// body to encode. Asserted by the "ignores --include-raw-output" integration test. +// `--include-raw-output` is accepted on `delete` but inert (only the deprecation warning fires): +// delete has no response body to encode. export const domainsDelete = Effect.fn("domains.delete")(function* (flags: DomainsDeleteFlags) { const output = yield* Output; const api = yield* CommandPlatformApi; @@ -22,6 +22,8 @@ export const domainsDelete = Effect.fn("domains.delete")(function* (flags: Domai const linkedProjectCache = yield* LinkedProjectCache; const telemetryState = yield* TelemetryState; + yield* warnIncludeRawOutputDeprecated(flags.includeRawOutput); + const ref = yield* resolver.resolve(flags.projectRef); yield* Effect.gen(function* () { diff --git a/apps/cli/src/commands/domains/delete/delete.integration.test.ts b/apps/cli/src/commands/domains/delete/delete.integration.test.ts index e874de9286..f7e62c7c80 100644 --- a/apps/cli/src/commands/domains/delete/delete.integration.test.ts +++ b/apps/cli/src/commands/domains/delete/delete.integration.test.ts @@ -43,7 +43,7 @@ function setup(opts: SetupOpts = {}) { return { layer, out, api, telemetry, linkedProjectCache }; } -const baseFlags = { projectRef: Option.none(), includeRawOutput: false }; +const baseFlags = { projectRef: Option.none(), includeRawOutput: Option.none() }; describe("domains delete integration", () => { it.effect("prints the success line to stderr in text mode", () => { @@ -84,12 +84,30 @@ describe("domains delete integration", () => { }).pipe(Effect.provide(layer)); }); - it.effect("ignores --include-raw-output (inert on delete, Go parity)", () => { + it.effect( + "is inert apart from the stderr deprecation warning when --include-raw-output is set", + () => { + const { layer, out } = setup({ goOutput: "json" }); + return Effect.gen(function* () { + yield* domainsDelete({ projectRef: Option.none(), includeRawOutput: Option.some(true) }); + expect(out.stdoutText).toBe(""); + expect(out.stderrText).toBe( + "Flag --include-raw-output is deprecated and will be removed in v3, use -o json instead.\n" + + "Deleted custom hostname config successfully.\n", + ); + }).pipe(Effect.provide(layer)); + }, + ); + + it.effect("warns when --include-raw-output=false", () => { const { layer, out } = setup(); return Effect.gen(function* () { - yield* domainsDelete({ projectRef: Option.none(), includeRawOutput: true }); + yield* domainsDelete({ projectRef: Option.none(), includeRawOutput: Option.some(false) }); expect(out.stdoutText).toBe(""); - expect(out.stderrText).toBe("Deleted custom hostname config successfully.\n"); + expect(out.stderrText).toBe( + "Flag --include-raw-output is deprecated and will be removed in v3, use -o json instead.\n" + + "Deleted custom hostname config successfully.\n", + ); }).pipe(Effect.provide(layer)); }); diff --git a/apps/cli/src/commands/domains/domains.deprecation.ts b/apps/cli/src/commands/domains/domains.deprecation.ts new file mode 100644 index 0000000000..324e208706 --- /dev/null +++ b/apps/cli/src/commands/domains/domains.deprecation.ts @@ -0,0 +1,16 @@ +import { Effect, Option } from "effect"; + +import { Output } from "../../shared/output/output.service.ts"; + +const INCLUDE_RAW_OUTPUT_DEPRECATION = + "Flag --include-raw-output is deprecated and will be removed in v3, use -o json instead.\n"; + +export const warnIncludeRawOutputDeprecated = Effect.fnUntraced(function* ( + includeRawOutput: Option.Option, +) { + if (Option.isNone(includeRawOutput)) { + return; + } + const output = yield* Output; + yield* output.raw(INCLUDE_RAW_OUTPUT_DEPRECATION, "stderr"); +}); diff --git a/apps/cli/src/commands/domains/get/get.command.ts b/apps/cli/src/commands/domains/get/get.command.ts index 03d0170ef3..c16551ad4f 100644 --- a/apps/cli/src/commands/domains/get/get.command.ts +++ b/apps/cli/src/commands/domains/get/get.command.ts @@ -13,7 +13,7 @@ const config = { ), includeRawOutput: Flag.Boolean("include-raw-output").pipe( Flag.withDescription("(Deprecated) use -o json instead."), - Flag.withDefault(false), + Flag.optional, ), } as const; diff --git a/apps/cli/src/commands/domains/get/get.handler.ts b/apps/cli/src/commands/domains/get/get.handler.ts index e1c1faa711..2d752e8098 100644 --- a/apps/cli/src/commands/domains/get/get.handler.ts +++ b/apps/cli/src/commands/domains/get/get.handler.ts @@ -1,10 +1,11 @@ -import { Effect } from "effect"; +import { Effect, Option } from "effect"; import { CommandPlatformApi } from "../../../auth/command-platform-api.service.ts"; import { ProjectRefResolver } from "../../../config/project-ref.service.ts"; import { Output } from "../../../shared/output/output.service.ts"; import { LinkedProjectCache } from "../../../telemetry/linked-project-cache.service.ts"; import { TelemetryState } from "../../../telemetry/telemetry-state.service.ts"; +import { warnIncludeRawOutputDeprecated } from "../domains.deprecation.ts"; import { emitHostnameResult } from "../domains.emit.ts"; import { mapDomainsHttpError } from "../domains.errors.ts"; import { gateMapError } from "../../../command-internal/upgrade-suggest.ts"; @@ -19,6 +20,8 @@ export const domainsGet = Effect.fn("domains.get")(function* (flags: DomainsGetF const linkedProjectCache = yield* LinkedProjectCache; const telemetryState = yield* TelemetryState; + yield* warnIncludeRawOutputDeprecated(flags.includeRawOutput); + const ref = yield* resolver.resolve(flags.projectRef); // Write the linked-project cache and persist the telemetry state file on @@ -34,6 +37,9 @@ export const domainsGet = Effect.fn("domains.get")(function* (flags: DomainsGetF ); yield* fetching?.clear ?? Effect.void; - yield* emitHostnameResult(response, flags.includeRawOutput); + yield* emitHostnameResult( + response, + Option.getOrElse(flags.includeRawOutput, () => false), + ); }).pipe(Effect.ensuring(linkedProjectCache.cache(ref)), Effect.ensuring(telemetryState.flush)); }); diff --git a/apps/cli/src/commands/domains/get/get.integration.test.ts b/apps/cli/src/commands/domains/get/get.integration.test.ts index df325190ba..86d38e6acd 100644 --- a/apps/cli/src/commands/domains/get/get.integration.test.ts +++ b/apps/cli/src/commands/domains/get/get.integration.test.ts @@ -78,7 +78,9 @@ function setup(opts: SetupOpts = {}) { return { layer, out, api, analytics, telemetry, linkedProjectCache }; } -const baseFlags = { projectRef: Option.none(), includeRawOutput: false }; +const baseFlags = { projectRef: Option.none(), includeRawOutput: Option.none() }; + +const DeprecationJsonObject = Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown)); describe("domains get integration", () => { it.effect("prints the hostname status to stderr in text mode", () => { @@ -336,12 +338,40 @@ describe("domains get integration", () => { }).pipe(Effect.provide(layer)); }); - it.effect("forces Go JSON output when --include-raw-output is set", () => { + it.effect( + "warns on stderr and keeps stdout JSON parseable when --include-raw-output is set", + () => { + const { layer, out } = setup(); + return Effect.gen(function* () { + yield* domainsGet({ projectRef: Option.none(), includeRawOutput: Option.some(true) }); + expect(out.stderrText).toBe( + "Flag --include-raw-output is deprecated and will be removed in v3, use -o json instead.\n", + ); + const parsed = yield* Schema.decodeEffect(DeprecationJsonObject)(out.stdoutText); + expect(parsed.custom_hostname).toBe("shop.acme.dev"); + expect(out.stdoutText).toContain('"custom_hostname": "shop.acme.dev"'); + }).pipe(Effect.provide(layer)); + }, + ); + + it.effect("warns but does not force JSON when --include-raw-output=false", () => { const { layer, out } = setup(); return Effect.gen(function* () { - yield* domainsGet({ projectRef: Option.none(), includeRawOutput: true }); - expect(out.stdoutText.startsWith("{")).toBe(true); - expect(out.stdoutText).toContain('"custom_hostname": "shop.acme.dev"'); + yield* domainsGet({ projectRef: Option.none(), includeRawOutput: Option.some(false) }); + expect( + out.stderrText.startsWith( + "Flag --include-raw-output is deprecated and will be removed in v3, use -o json instead.\n", + ), + ).toBe(true); + expect(out.stdoutText).toBe(""); + }).pipe(Effect.provide(layer)); + }); + + it.effect("does not warn when --include-raw-output is not set", () => { + const { layer, out } = setup(); + return Effect.gen(function* () { + yield* domainsGet(baseFlags); + expect(out.stderrText).not.toContain("deprecated"); }).pipe(Effect.provide(layer)); }); @@ -350,7 +380,7 @@ describe("domains get integration", () => { () => { const { layer, out } = setup({ goOutput: "pretty" }); return Effect.gen(function* () { - yield* domainsGet({ projectRef: Option.none(), includeRawOutput: true }); + yield* domainsGet({ projectRef: Option.none(), includeRawOutput: Option.some(true) }); expect(out.stdoutText.startsWith("{")).toBe(true); }).pipe(Effect.provide(layer)); }, diff --git a/apps/cli/src/commands/domains/reverify/reverify.command.ts b/apps/cli/src/commands/domains/reverify/reverify.command.ts index 0565b0126f..8f686eaa02 100644 --- a/apps/cli/src/commands/domains/reverify/reverify.command.ts +++ b/apps/cli/src/commands/domains/reverify/reverify.command.ts @@ -13,7 +13,7 @@ const config = { ), includeRawOutput: Flag.Boolean("include-raw-output").pipe( Flag.withDescription("(Deprecated) use -o json instead."), - Flag.withDefault(false), + Flag.optional, ), } as const; diff --git a/apps/cli/src/commands/domains/reverify/reverify.handler.ts b/apps/cli/src/commands/domains/reverify/reverify.handler.ts index 6b2e47c58d..a7b10b9dee 100644 --- a/apps/cli/src/commands/domains/reverify/reverify.handler.ts +++ b/apps/cli/src/commands/domains/reverify/reverify.handler.ts @@ -1,10 +1,11 @@ -import { Effect } from "effect"; +import { Effect, Option } from "effect"; import { CommandPlatformApi } from "../../../auth/command-platform-api.service.ts"; import { ProjectRefResolver } from "../../../config/project-ref.service.ts"; import { Output } from "../../../shared/output/output.service.ts"; import { LinkedProjectCache } from "../../../telemetry/linked-project-cache.service.ts"; import { TelemetryState } from "../../../telemetry/telemetry-state.service.ts"; +import { warnIncludeRawOutputDeprecated } from "../domains.deprecation.ts"; import { emitHostnameResult } from "../domains.emit.ts"; import { mapDomainsHttpError } from "../domains.errors.ts"; import { gateMapError } from "../../../command-internal/upgrade-suggest.ts"; @@ -21,6 +22,8 @@ export const domainsReverify = Effect.fn("domains.reverify")(function* ( const linkedProjectCache = yield* LinkedProjectCache; const telemetryState = yield* TelemetryState; + yield* warnIncludeRawOutputDeprecated(flags.includeRawOutput); + const ref = yield* resolver.resolve(flags.projectRef); yield* Effect.gen(function* () { @@ -32,6 +35,9 @@ export const domainsReverify = Effect.fn("domains.reverify")(function* ( ); yield* reverifying?.clear ?? Effect.void; - yield* emitHostnameResult(response, flags.includeRawOutput); + yield* emitHostnameResult( + response, + Option.getOrElse(flags.includeRawOutput, () => false), + ); }).pipe(Effect.ensuring(linkedProjectCache.cache(ref)), Effect.ensuring(telemetryState.flush)); }); diff --git a/apps/cli/src/commands/domains/reverify/reverify.integration.test.ts b/apps/cli/src/commands/domains/reverify/reverify.integration.test.ts index ad3965fb74..d5e24c50b0 100644 --- a/apps/cli/src/commands/domains/reverify/reverify.integration.test.ts +++ b/apps/cli/src/commands/domains/reverify/reverify.integration.test.ts @@ -1,6 +1,6 @@ import { type V1GetHostnameConfigOutput } from "@supabase/api/effect"; import { describe, expect, it } from "@effect/vitest"; -import { Cause, Effect, Exit, Option } from "effect"; +import { Cause, Effect, Exit, Option, Schema } from "effect"; import { mockAnalytics, mockOutput } from "../../../../tests/helpers/mocks.ts"; import { @@ -65,7 +65,9 @@ function setup(opts: SetupOpts = {}) { return { layer, out, api, analytics, telemetry, linkedProjectCache }; } -const baseFlags = { projectRef: Option.none(), includeRawOutput: false }; +const baseFlags = { projectRef: Option.none(), includeRawOutput: Option.none() }; + +const DeprecationJsonObject = Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown)); describe("domains reverify integration", () => { it.effect("suggests upgrade from entitlement_required envelope on 400", () => { @@ -126,11 +128,39 @@ describe("domains reverify integration", () => { }).pipe(Effect.provide(layer)); }); - it.effect("forces Go JSON output when --include-raw-output is set", () => { + it.effect( + "warns on stderr and keeps stdout JSON parseable when --include-raw-output is set", + () => { + const { layer, out } = setup(); + return Effect.gen(function* () { + yield* domainsReverify({ projectRef: Option.none(), includeRawOutput: Option.some(true) }); + expect(out.stderrText).toBe( + "Flag --include-raw-output is deprecated and will be removed in v3, use -o json instead.\n", + ); + const parsed = yield* Schema.decodeEffect(DeprecationJsonObject)(out.stdoutText); + expect(parsed.custom_hostname).toBe("shop.acme.dev"); + }).pipe(Effect.provide(layer)); + }, + ); + + it.effect("warns but does not force JSON when --include-raw-output=false", () => { const { layer, out } = setup(); return Effect.gen(function* () { - yield* domainsReverify({ projectRef: Option.none(), includeRawOutput: true }); - expect(out.stdoutText.startsWith("{")).toBe(true); + yield* domainsReverify({ projectRef: Option.none(), includeRawOutput: Option.some(false) }); + expect( + out.stderrText.startsWith( + "Flag --include-raw-output is deprecated and will be removed in v3, use -o json instead.\n", + ), + ).toBe(true); + expect(out.stdoutText).toBe(""); + }).pipe(Effect.provide(layer)); + }); + + it.effect("does not warn when --include-raw-output is not set", () => { + const { layer, out } = setup(); + return Effect.gen(function* () { + yield* domainsReverify(baseFlags); + expect(out.stderrText).not.toContain("deprecated"); }).pipe(Effect.provide(layer)); });