diff --git a/apps/cli/src/command-internal/resource-output.ts b/apps/cli/src/command-internal/resource-output.ts new file mode 100644 index 0000000000..1954ca0410 --- /dev/null +++ b/apps/cli/src/command-internal/resource-output.ts @@ -0,0 +1,44 @@ +import { Effect, Option } from "effect"; +import { OutputFlag } from "./global-flags.ts"; +import { Output } from "../shared/output/output.service.ts"; +import { encodeGoJson, encodeToml, encodeYaml } from "./go-output.encoders.ts"; + +const payloadFormats = new Set(["json", "yaml", "toml", "env"]); + +/** Output policy for structured resources without a command-specific encoding. */ +export function resourceOutput(EnvNotSupportedError: new (args: { message: string }) => E) { + const requested = Effect.gen(function* () { + const format = Option.getOrUndefined(yield* OutputFlag); + return format !== undefined && payloadFormats.has(format); + }); + + // Call before mutations; emit also checks as a backstop. + const rejectEnv = Effect.gen(function* () { + if (Option.getOrUndefined(yield* OutputFlag) === "env") { + return yield* Effect.fail( + new EnvNotSupportedError({ message: "--output env flag is not supported" }), + ); + } + }); + + const emit = Effect.fnUntraced(function* (payload: Record) { + if (!(yield* requested)) return false; + yield* rejectEnv; + const output = yield* Output; + const format = Option.getOrUndefined(yield* OutputFlag); + switch (format) { + case "json": + yield* output.raw(encodeGoJson(payload)); + break; + case "yaml": + yield* output.raw(encodeYaml(payload)); + break; + case "toml": + yield* output.raw(encodeToml(payload)); + break; + } + return true; + }); + + return { emit, requested: () => requested, rejectEnv: () => rejectEnv }; +} diff --git a/apps/cli/src/commands/experimental/compute/compute.output.ts b/apps/cli/src/commands/experimental/compute/compute.output.ts index cd2dc04aa3..f3059c4a8b 100644 --- a/apps/cli/src/commands/experimental/compute/compute.output.ts +++ b/apps/cli/src/commands/experimental/compute/compute.output.ts @@ -1,65 +1,14 @@ import { Effect, Option } from "effect"; import { OutputFlag } from "../../../command-internal/global-flags.ts"; +import { resourceOutput } from "../../../command-internal/resource-output.ts"; import { Output } from "../../../shared/output/output.service.ts"; -import { - encodeGoJson, - encodeToml, - encodeYaml, -} from "../../../command-internal/go-output.encoders.ts"; import { ComputeEnvNotSupportedError } from "./compute.errors.ts"; -/** - * Which `-o` values these commands answer with a payload. - * - * An allowlist, not a denylist, so an unrecognized future `-o` value falls - * through to text instead of silently serializing as TOML. `env` is included - * so it reaches the refusal below rather than being treated as unrecognized. - */ -const PAYLOAD_FORMATS = new Set(["json", "yaml", "toml", "env"]); - -function emitsPayloadFor(goFormat: string | undefined): boolean { - return goFormat !== undefined && PAYLOAD_FORMATS.has(goFormat); -} - -export const emitComputeMachineOutput = Effect.fnUntraced(function* ( - payload: Record, -) { - const output = yield* Output; - const goFormat = Option.getOrUndefined(yield* OutputFlag); - - if (!emitsPayloadFor(goFormat)) { - return false; - } - - if (goFormat === "env") { - // Unreachable when the command called `rejectComputeEnvOutput` first, - // which is where the refusal belongs; here as the backstop that stops a new - // command silently emitting TOML for `-o env`. - return yield* new ComputeEnvNotSupportedError({ - message: "--output env flag is not supported", - }); - } - - if (goFormat === "json") { - yield* output.raw(encodeGoJson(payload)); - return true; - } - if (goFormat === "yaml") { - yield* output.raw(encodeYaml(payload)); - return true; - } - yield* output.raw(encodeToml(payload)); - return true; -}); - -/** - * Whether a machine-readable stdout was requested via `-o`. Callers that emit - * human lines *before* their payload need this: the `-o` branch runs at the end, - * by which point those lines would already be on stdout. - */ -export const computeMachineOutputRequested = Effect.fnUntraced(function* () { - return emitsPayloadFor(Option.getOrUndefined(yield* OutputFlag)); -}); +export const { + emit: emitComputeMachineOutput, + requested: computeMachineOutputRequested, + rejectEnv: rejectComputeEnvOutput, +} = resourceOutput(ComputeEnvNotSupportedError); /** * The format a run actually renders in, with `-o` given priority over @@ -73,25 +22,10 @@ export const computeMachineOutputRequested = Effect.fnUntraced(function* () { export const computeRenderFormat = Effect.fnUntraced(function* () { const output = yield* Output; const goFormat = Option.getOrUndefined(yield* OutputFlag); - const forcesText = goFormat !== undefined && !emitsPayloadFor(goFormat); + const forcesText = goFormat !== undefined && !(yield* computeMachineOutputRequested()); return forcesText ? ("text" as const) : output.format; }); -/** - * Refuses `-o env` before the command does anything. - * - * Every compute payload has structure a flat `KEY=value` list cannot hold. - * Refused up front rather than at emit time, since for `push` that would mean - * failing only after the remote project has already changed. - */ -export const rejectComputeEnvOutput = Effect.fnUntraced(function* () { - if (Option.getOrUndefined(yield* OutputFlag) === "env") { - return yield* new ComputeEnvNotSupportedError({ - message: "--output env flag is not supported", - }); - } -}); - /** * The `--project-ref` a retry suggestion has to carry, or `""` when it came * from the link.