Skip to content

Commit a4b4b3a

Browse files
committed
fix(cli): preserve legacy agent output controls
1 parent 47eb743 commit a4b4b3a

9 files changed

Lines changed: 168 additions & 7 deletions

File tree

apps/cli/src/legacy/cli/agent-output.e2e.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,61 @@ describe("legacy CLI agent output", () => {
6666
expect(stdout).toContain("DESCRIPTION");
6767
expect(stderr).toContain('Unknown subcommand "definitely-not-a-command"');
6868
});
69+
70+
test("keeps parse errors in text mode when --agent=no is explicit", async () => {
71+
const { exitCode, stdout, stderr } = await runSupabase(
72+
["--agent", "no", "definitely-not-a-command"],
73+
{
74+
entrypoint: "legacy",
75+
env: { CODEX_SANDBOX: "1" },
76+
},
77+
);
78+
79+
expect(exitCode).toBe(1);
80+
expect(stdout).toContain("DESCRIPTION");
81+
expect(stderr).toContain('Unknown subcommand "definitely-not-a-command"');
82+
});
83+
84+
test("formats parse errors as JSON when --agent=yes is explicit", async () => {
85+
const { exitCode, stdout, stderr } = await runSupabase(
86+
["--agent", "yes", "definitely-not-a-command"],
87+
{
88+
entrypoint: "legacy",
89+
env: {},
90+
},
91+
);
92+
93+
expect(exitCode).toBe(1);
94+
expect(parseJsonLines(stdout)).toEqual([
95+
expect.objectContaining({ _tag: "Help" }),
96+
expect.objectContaining({
97+
_tag: "Error",
98+
error: expect.objectContaining({ code: "ShowHelp" }),
99+
}),
100+
]);
101+
expect(parseJsonLines(stderr)).toEqual([
102+
expect.objectContaining({
103+
_tag: "Errors",
104+
errors: [expect.objectContaining({ code: "UnknownSubcommand" })],
105+
}),
106+
]);
107+
});
108+
109+
test("keeps built-in version and help in text mode for detected coding agents", async () => {
110+
const version = await runSupabase(["--version"], {
111+
entrypoint: "legacy",
112+
env: { CODEX_SANDBOX: "1" },
113+
});
114+
const help = await runSupabase(["--help"], {
115+
entrypoint: "legacy",
116+
env: { CODEX_SANDBOX: "1" },
117+
});
118+
119+
expect(version.exitCode).toBe(0);
120+
expect(version.stdout.trim()).toMatch(/^\d+\.\d+\.\d+/);
121+
expect(() => JSON.parse(version.stdout)).toThrow();
122+
expect(help.exitCode).toBe(0);
123+
expect(help.stdout).toContain("DESCRIPTION");
124+
expect(() => JSON.parse(help.stdout)).toThrow();
125+
});
69126
});

apps/cli/src/legacy/cli/root.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,11 @@ import { legacyQuietProgressTextOutputLayer } from "../output/legacy-quiet-progr
4141
import { makeGoProxyLayer } from "../../shared/legacy/go-proxy.layer.ts";
4242
import { AiTool } from "../../shared/telemetry/ai-tool.service.ts";
4343
import { aiToolLayer } from "../../shared/telemetry/ai-tool.layer.ts";
44-
import { resolveAgentOutputFormat } from "../../shared/cli/agent-output.ts";
44+
import { CliArgs } from "../../shared/cli/cli-args.service.ts";
45+
import { isBuiltInTextRequest, resolveAgentOutputFormat } from "../../shared/cli/agent-output.ts";
4546
import {
4647
LEGACY_GLOBAL_FLAGS,
48+
LegacyAgentFlag,
4749
LegacyCreateTicketFlag,
4850
LegacyDebugFlag,
4951
LegacyDnsResolverFlag,
@@ -108,6 +110,8 @@ export const legacyRoot = Command.make("supabase").pipe(
108110
const yes = yield* LegacyYesFlag;
109111
const dnsResolver = yield* LegacyDnsResolverFlag;
110112
const createTicket = yield* LegacyCreateTicketFlag;
113+
const agent = yield* LegacyAgentFlag;
114+
const cliArgs = yield* CliArgs;
111115

112116
const aiTool = yield* AiTool.pipe(Effect.provide(aiToolLayer));
113117
// An explicit Go --output is a complete format choice (even `-o pretty`
@@ -116,7 +120,9 @@ export const legacyRoot = Command.make("supabase").pipe(
116120
const outputFormat = resolveAgentOutputFormat({
117121
explicitOutputFormat,
118122
legacyOutputFormat: goOutput,
123+
agentOverride: agent,
119124
detectedAgentName: aiTool.name,
125+
isBuiltInTextRequest: isBuiltInTextRequest(cliArgs.args),
120126
});
121127

122128
// Build args to prepend to every proxy exec call.
@@ -135,6 +141,7 @@ export const legacyRoot = Command.make("supabase").pipe(
135141
if (yes) globalArgs.push("--yes");
136142
if (dnsResolver !== "native") globalArgs.push("--dns-resolver", dnsResolver);
137143
if (createTicket) globalArgs.push("--create-ticket");
144+
if (agent !== "auto") globalArgs.push("--agent", agent);
138145

139146
// Go's `-o {json,yaml,toml,env}` selects a machine encoder the handler
140147
// writes via `output.raw`. Keep the text layer (so errors still render

apps/cli/src/next/cli/root.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { CliOutput, Command } from "effect/unstable/cli";
33
import { OutputFormatFlag } from "../../shared/cli/global-flags.ts";
44
import { AiTool } from "../../shared/telemetry/ai-tool.service.ts";
55
import { aiToolLayer } from "../../shared/telemetry/ai-tool.layer.ts";
6-
import { resolveAgentOutputFormat } from "../../shared/cli/agent-output.ts";
6+
import { isBuiltInTextRequest, resolveAgentOutputFormat } from "../../shared/cli/agent-output.ts";
7+
import { CliArgs } from "../../shared/cli/cli-args.service.ts";
78
import { branchesCommand } from "../commands/branches/branches.command.ts";
89
import { functionsCommand } from "../commands/functions/functions.command.ts";
910
import { linkCommand } from "../commands/link/link.command.ts";
@@ -51,10 +52,12 @@ export const nextRoot = Command.make("supabase").pipe(
5152
Layer.unwrap(
5253
Effect.gen(function* () {
5354
const explicitOutputFormat = yield* OutputFormatFlag;
55+
const cliArgs = yield* CliArgs;
5456
const aiTool = yield* AiTool.pipe(Effect.provide(aiToolLayer));
5557
const outputFormat = resolveAgentOutputFormat({
5658
explicitOutputFormat,
5759
detectedAgentName: aiTool.name,
60+
isBuiltInTextRequest: isBuiltInTextRequest(cliArgs.args),
5861
});
5962
const base = outputLayerFor(outputFormat);
6063
if (outputFormat === "text") return base;

apps/cli/src/shared/cli/agent-output.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ import { Option } from "effect";
22
import type { OutputFormat } from "../output/types.ts";
33

44
type LegacyOutputFormat = "env" | "pretty" | "json" | "toml" | "yaml";
5+
type AgentOverride = "auto" | "yes" | "no";
56

67
interface AgentOutputOptions {
78
readonly explicitOutputFormat: Option.Option<OutputFormat>;
89
readonly legacyOutputFormat?: Option.Option<LegacyOutputFormat>;
10+
readonly agentOverride?: AgentOverride;
911
readonly detectedAgentName?: Option.Option<string>;
12+
readonly isBuiltInTextRequest?: boolean;
1013
}
1114

1215
function readLongFlag(args: ReadonlyArray<string>, name: string): string | undefined {
@@ -70,13 +73,33 @@ function legacyOutputFormatFromArg(value: string | undefined): Option.Option<Leg
7073
}
7174
}
7275

76+
function agentOverrideFromArg(value: string | undefined): AgentOverride {
77+
switch (value) {
78+
case "yes":
79+
case "no":
80+
return value;
81+
default:
82+
return "auto";
83+
}
84+
}
85+
86+
export function isBuiltInTextRequest(args: ReadonlyArray<string>): boolean {
87+
return args.some(
88+
(arg) => arg === "--help" || arg === "-h" || arg === "--version" || arg === "-v",
89+
);
90+
}
91+
7392
export function resolveAgentOutputFormat(options: AgentOutputOptions): OutputFormat {
7493
const legacyOutputFormat = options.legacyOutputFormat ?? Option.none<LegacyOutputFormat>();
7594
const detectedAgentName = options.detectedAgentName ?? Option.none<string>();
76-
const isCodingAgent = Option.isSome(detectedAgentName);
95+
const agentOverride = options.agentOverride ?? "auto";
96+
const isCodingAgent =
97+
agentOverride === "yes" || (agentOverride === "auto" && Option.isSome(detectedAgentName));
7798

7899
return Option.getOrElse(options.explicitOutputFormat, () =>
79-
isCodingAgent && Option.isNone(legacyOutputFormat) ? "json" : "text",
100+
isCodingAgent && Option.isNone(legacyOutputFormat) && !options.isBuiltInTextRequest
101+
? "json"
102+
: "text",
80103
);
81104
}
82105

@@ -86,10 +109,13 @@ export function resolveAgentOutputFormatFromArgs(
86109
): OutputFormat {
87110
const explicitOutputFormat = outputFormatFromArg(readLongFlag(args, "--output-format"));
88111
const legacyOutputFormat = legacyOutputFormatFromArg(readOutputFlag(args));
112+
const agentOverride = agentOverrideFromArg(readLongFlag(args, "--agent"));
89113

90114
return resolveAgentOutputFormat({
91115
explicitOutputFormat,
92116
legacyOutputFormat,
117+
agentOverride,
93118
detectedAgentName,
119+
isBuiltInTextRequest: isBuiltInTextRequest(args),
94120
});
95121
}

apps/cli/src/shared/cli/agent-output.unit.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,40 @@ describe("resolveAgentOutputFormat", () => {
5252
).toBe("text");
5353
});
5454

55+
it("honors the legacy --agent override", () => {
56+
expect(
57+
resolveAgentOutputFormat({
58+
explicitOutputFormat: Option.none(),
59+
agentOverride: "no",
60+
detectedAgentName: Option.some("codex"),
61+
}),
62+
).toBe("text");
63+
expect(
64+
resolveAgentOutputFormat({
65+
explicitOutputFormat: Option.none(),
66+
agentOverride: "yes",
67+
detectedAgentName: Option.none(),
68+
}),
69+
).toBe("json");
70+
});
71+
72+
it("keeps built-in help and version text unless output-format is explicit", () => {
73+
expect(
74+
resolveAgentOutputFormat({
75+
explicitOutputFormat: Option.none(),
76+
detectedAgentName: Option.some("codex"),
77+
isBuiltInTextRequest: true,
78+
}),
79+
).toBe("text");
80+
expect(
81+
resolveAgentOutputFormat({
82+
explicitOutputFormat: Option.some("json"),
83+
detectedAgentName: Option.some("codex"),
84+
isBuiltInTextRequest: true,
85+
}),
86+
).toBe("json");
87+
});
88+
5589
it("resolves the effective format from raw argv for runtime error formatting", () => {
5690
expect(resolveAgentOutputFormatFromArgs(["bad-command"], Option.some("codex"))).toBe("json");
5791
expect(
@@ -69,5 +103,16 @@ describe("resolveAgentOutputFormat", () => {
69103
Option.some("codex"),
70104
),
71105
).toBe("stream-json");
106+
expect(
107+
resolveAgentOutputFormatFromArgs(["--agent", "no", "bad-command"], Option.some("codex")),
108+
).toBe("text");
109+
expect(resolveAgentOutputFormatFromArgs(["--agent=yes", "bad-command"], Option.none())).toBe(
110+
"json",
111+
);
112+
expect(resolveAgentOutputFormatFromArgs(["--version"], Option.some("codex"))).toBe("text");
113+
expect(resolveAgentOutputFormatFromArgs(["--help"], Option.some("codex"))).toBe("text");
114+
expect(
115+
resolveAgentOutputFormatFromArgs(["--output-format=json", "--help"], Option.some("codex")),
116+
).toBe("json");
72117
});
73118
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Context } from "effect";
2+
3+
export interface CliArgsShape {
4+
readonly args: ReadonlyArray<string>;
5+
}
6+
7+
export class CliArgs extends Context.Service<CliArgs, CliArgsShape>()("supabase/cli/CliArgs") {}

apps/cli/src/shared/cli/run.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { aiToolLayer } from "../telemetry/ai-tool.layer.ts";
2626
import { AiTool } from "../telemetry/ai-tool.service.ts";
2727
import { telemetryRuntimeLayer } from "../telemetry/runtime.layer.ts";
2828
import { tracingLayer } from "../telemetry/tracing.layer.ts";
29+
import { CliArgs } from "./cli-args.service.ts";
2930
import { resolveAgentOutputFormatFromArgs } from "./agent-output.ts";
3031

3132
function formatterLayerFor(format: OutputFormat) {
@@ -102,6 +103,7 @@ function cliProgramFor(
102103
Effect.provide(runtimeLayer),
103104
Effect.provide(unixHttpClientLayer),
104105
Effect.provide(fallbackCommandLayer),
106+
Effect.provide(Layer.succeed(CliArgs, { args })),
105107
Effect.provide(BunServices.layer),
106108
);
107109
}

apps/cli/src/shared/cli/version.integration.test.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,15 @@ import { vi } from "vitest";
66
import { legacyRoot } from "../../legacy/cli/root.ts";
77
import { nextRoot } from "../../next/cli/root.ts";
88
import { textCliOutputFormatter } from "../output/text-formatter.ts";
9+
import { CliArgs } from "./cli-args.service.ts";
910

1011
describe("CLI --version (text)", () => {
11-
const versionLayer = Layer.mergeAll(CliOutput.layer(textCliOutputFormatter()), BunServices.layer);
12+
const versionLayer = (args: ReadonlyArray<string>) =>
13+
Layer.mergeAll(
14+
CliOutput.layer(textCliOutputFormatter()),
15+
Layer.succeed(CliArgs, { args }),
16+
BunServices.layer,
17+
);
1218

1319
test("legacy shell prints bare semver on stdout", async () => {
1420
const logs: string[] = [];
@@ -28,7 +34,7 @@ describe("CLI --version (text)", () => {
2834
// `--version` exits early; only BunServices + CliOutput are needed at runtime here.
2935
await Effect.runPromise(
3036
Command.runWith(legacyRoot, { version: "2.99.0-beta.1" })(["--version"]).pipe(
31-
Effect.provide(versionLayer),
37+
Effect.provide(versionLayer(["--version"])),
3238
) as Effect.Effect<void>,
3339
);
3440
} finally {
@@ -55,7 +61,7 @@ describe("CLI --version (text)", () => {
5561
try {
5662
await Effect.runPromise(
5763
Command.runWith(nextRoot, { version: "2.99.0-beta.1" })(["--version"]).pipe(
58-
Effect.provide(versionLayer),
64+
Effect.provide(versionLayer(["--version"])),
5965
) as Effect.Effect<void>,
6066
);
6167
} finally {

apps/cli/src/shared/legacy/global-flags.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ export const LegacyCreateTicketFlag = GlobalFlag.setting("create-ticket")({
5454
),
5555
});
5656

57+
export const LegacyAgentFlag = GlobalFlag.setting("agent")({
58+
flag: Flag.choice("agent", ["auto", "yes", "no"] as const).pipe(
59+
Flag.withDescription("Override agent detection: yes, no, or auto (default auto)."),
60+
Flag.withDefault("auto" as const),
61+
),
62+
});
63+
5764
export const LEGACY_GLOBAL_FLAGS = [
5865
LegacyOutputFlag,
5966
LegacyProfileFlag,
@@ -64,4 +71,5 @@ export const LEGACY_GLOBAL_FLAGS = [
6471
LegacyYesFlag,
6572
LegacyDnsResolverFlag,
6673
LegacyCreateTicketFlag,
74+
LegacyAgentFlag,
6775
] as const;

0 commit comments

Comments
 (0)