diff --git a/apps/cli-e2e/src/tests/gen.e2e.test.ts b/apps/cli-e2e/src/tests/gen.e2e.test.ts index 3199feac2..cdb173382 100644 --- a/apps/cli-e2e/src/tests/gen.e2e.test.ts +++ b/apps/cli-e2e/src/tests/gen.e2e.test.ts @@ -12,10 +12,16 @@ describe("gen", () => { testBehaviour.skipIf(isRecording)( "generates typescript types from project", async ({ run, projectRef }) => { - const result = await run(["gen", "types", "--project-id", projectRef]); + // #5212 — with CLAUDECODE=1, piped/redirected output must not include the + // plugin hint (would break `gen types > file.ts` and similar captures). + const result = await run(["gen", "types", "--project-id", projectRef], { + env: { CLAUDECODE: "1" }, + }); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("export type Json"); expect(result.stdout).toContain("export type Database"); + expect(result.stdout).not.toMatch(/claude-code-hint/); + expect(result.stderr).not.toMatch(/claude-code-hint/); }, ); diff --git a/apps/cli-e2e/src/tests/test-context.ts b/apps/cli-e2e/src/tests/test-context.ts index 63036b1ef..613360f34 100644 --- a/apps/cli-e2e/src/tests/test-context.ts +++ b/apps/cli-e2e/src/tests/test-context.ts @@ -21,11 +21,13 @@ function scenarioSlug(task: { name: string; suite?: { name: string } | null }): return prefix + slugify(task.name); } +type ExecOptions = NonNullable[2]>; + interface BehaviourFixtures { projectRef: string; orgId: string; workspace: TempDir; - run: (cmd: string[]) => Promise; + run: (cmd: string[], execOpts?: ExecOptions) => Promise; runNoProjectId: (cmd: string[]) => Promise; apiUrl: string; storageBucket: string; @@ -38,7 +40,8 @@ interface BehaviourFixtures { * - `projectRef` — a real project ref (record mode) or the replay default * - `orgId` — a real org slug (record mode) or the replay default * - `workspace` — fresh temp dir, auto-disposed after the test - * - `run` — pre-configured `exec()` for the current TARGET + * - `run` — pre-configured `exec()` for the current TARGET (optional second + * argument forwarded as `exec` options, e.g. extra `env` entries) * - `apiUrl` — the replay server base URL (for setting up error overrides) * * Auto-wires a named scenario for the test before running it, so the replay @@ -90,7 +93,7 @@ export const testBehaviour = test.extend({ cwd: workspace.path, projectId: inject("projectRef") as string, }); - await use((cmd) => exec(harness, cmd)); + await use((cmd, execOpts) => exec(harness, cmd, execOpts)); }, runNoProjectId: async ({ workspace }, use) => { diff --git a/apps/cli-go/internal/utils/misc.go b/apps/cli-go/internal/utils/misc.go index 7ba482d96..6e164acd9 100644 --- a/apps/cli-go/internal/utils/misc.go +++ b/apps/cli-go/internal/utils/misc.go @@ -19,6 +19,7 @@ import ( "github.com/spf13/viper" "github.com/supabase/cli/internal/utils/agent" "github.com/supabase/cli/pkg/migration" + "golang.org/x/term" ) // Assigned using `-ldflags` https://stackoverflow.com/q/11354518 @@ -42,10 +43,17 @@ const SuggestDebugFlag = "Try rerunning the command with --debug to troubleshoot const claudeCodeHint = `` func SuggestClaudePlugin() string { - if agent.IsClaudeCode() { - return claudeCodeHint + if !agent.IsClaudeCode() { + return "" } - return "" + // Suppress the hint when stdout is non-interactive (redirected to a file + // or piped). Without this guard, captured output could be contaminated by + // the trailer if anything ever leaks the tag onto stdout, and the + // install-prompt UX is only meaningful in an interactive shell anyway. + if !term.IsTerminal(int(os.Stdout.Fd())) { //nolint:gosec // G115: stdout fd is a small int on supported platforms + return "" + } + return claudeCodeHint } var (