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
18 changes: 12 additions & 6 deletions apps/cli/src/legacy/commands/bootstrap/bootstrap.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { LegacyCliConfig } from "../../config/legacy-cli-config.service.ts";
import { LegacyLinkedProjectCache } from "../../telemetry/legacy-linked-project-cache.service.ts";
import { LegacyTelemetryState } from "../../telemetry/legacy-telemetry-state.service.ts";
import { LegacyWorkdirFlag, legacyResolveYes } from "../../../shared/legacy/global-flags.ts";
import { legacyPromptYesNo } from "../../../shared/legacy/legacy-prompt-yes-no.ts";
import { CONTEXT_CANCELED_MESSAGE } from "../../../shared/output/errors.ts";
import { Output } from "../../../shared/output/output.service.ts";
import { LegacyGoProxy } from "../../../shared/legacy/go-proxy.service.ts";
Expand Down Expand Up @@ -129,12 +130,16 @@ export const legacyBootstrap = Effect.fn("legacy.bootstrap")(function* (
),
);
if (entries.length > 0) {
const overwrite = yesFlag
? true
: yield* output.promptConfirm(
`Do you want to overwrite existing files in ${legacyBold(workdir)} directory?`,
{ defaultValue: true },
);
// Go's `PromptYesNo(title, true)` (`bootstrap.go:47-48`, `console.go:64-82`):
// `--yes`/`SUPABASE_YES` auto-confirms with the `<title> [Y/n] y` stderr echo
// instead of silently skipping the prompt, and a non-TTY stdin scans one
// piped line (100ms) before falling back to the Yes default (CLI-1974).
const overwrite = yield* legacyPromptYesNo(
output,
yesFlag,
`Do you want to overwrite existing files in ${legacyBold(workdir)} directory?`,
true,
);
if (!overwrite) {
return yield* new LegacyBootstrapOverwriteDeclinedError({
message: CONTEXT_CANCELED_MESSAGE,
Expand All @@ -159,6 +164,7 @@ export const legacyBootstrap = Effect.fn("legacy.bootstrap")(function* (
cwd: workdir,
force: true,
interactive: false,
yes: yesFlag,
useOrioledb: false,
withVscodeSettings: false,
withIntellijSettings: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,10 @@ describe("legacy bootstrap integration", () => {
writeFileSync(join(tempRoot.current, "existing.txt"), "keep me");
return Effect.gen(function* () {
yield* legacyBootstrap(flags({ template: Option.some("scratch") }), FAST_BACKOFF);
// Go's PromptYesNo echoes the auto-accepted overwrite question to stderr
// under the global YES flag (`bootstrap.go:47-48`, `console.go:70-72`).
expect(s.out.stderrText).toContain("Do you want to overwrite existing files in ");
expect(s.out.stderrText).toContain(" directory? [Y/n] y\n");
expect(existsSync(join(s.workdir, "supabase", "config.toml"))).toBe(true);
}).pipe(Effect.provide(s.layer));
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Argument, Command, Flag } from "effect/unstable/cli";
import type * as CliCommand from "effect/unstable/cli/Command";

import { Layer } from "effect";
import { withJsonErrorHandling } from "../../../../shared/output/json-error-handling.ts";
import { stdinLayer } from "../../../../shared/runtime/stdin.layer.ts";
import { legacyManagementApiRuntimeLayer } from "../../../shared/legacy-management-api-runtime.layer.ts";
import { withLegacyCommandInstrumentation } from "../../../telemetry/legacy-command-instrumentation.ts";
import { legacyBranchesCreate } from "./create.handler.ts";
Expand Down Expand Up @@ -99,5 +101,9 @@ export const legacyBranchesCreateCommand = Command.make("create", config).pipe(
withJsonErrorHandling,
),
),
Command.provide(legacyManagementApiRuntimeLayer(["branches", "create"])),
// `stdinLayer`: the confirmation prompt reads piped stdin via `legacyPromptYesNo`
// (Go's `Console.ReadLine`, `console.go:38-61`) on a non-TTY stdin.
Command.provide(
Layer.mergeAll(legacyManagementApiRuntimeLayer(["branches", "create"]), stdinLayer),
),
);
25 changes: 16 additions & 9 deletions apps/cli/src/legacy/commands/branches/create/create.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import { LegacyPlatformApi } from "../../../auth/legacy-platform-api.service.ts"
import { LegacyProjectRefResolver } from "../../../config/legacy-project-ref.service.ts";
import { LegacyLinkedProjectCache } from "../../../telemetry/legacy-linked-project-cache.service.ts";
import { LegacyTelemetryState } from "../../../telemetry/legacy-telemetry-state.service.ts";
import { LegacyOutputFlag } from "../../../../shared/legacy/global-flags.ts";
import { LegacyOutputFlag, legacyResolveYes } from "../../../../shared/legacy/global-flags.ts";
import { legacyPromptYesNo } from "../../../../shared/legacy/legacy-prompt-yes-no.ts";
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,
Expand Down Expand Up @@ -60,14 +62,19 @@ export const legacyBranchesCreate = Effect.fn("legacy.branches.create")(function
if (branchName.length === 0) {
const gitBranch = yield* detectGitBranch();
if (Option.isSome(gitBranch) && gitBranch.value.length > 0) {
// Go's `create.go:20-25` calls `utils.NewConsole().PromptYesNo(...)`
// unconditionally — on a TTY it blocks for input, off-TTY it reads stdin
// with a 100ms timeout and defaults to `true` on EOF. We always fire the
// confirm; the non-interactive `Output` layer auto-falls-through (via
// `Effect.orElseSucceed(true)`) which matches Go's EOF-default-true.
const confirmed = yield* output
.promptConfirm(`Do you want to create a branch named ${gitBranch.value}?`)
.pipe(Effect.orElseSucceed(() => true));
// Go's `create.go:20-25` routes this through `PromptYesNo(title, true)`
// (`console.go:64-82`), so `--yes`/`SUPABASE_YES` auto-confirms with the
// `<title> [Y/n] y` stderr echo instead of blocking a TTY, and a non-TTY
// stdin prints the label and scans one piped line (100ms) before falling
// back to the Yes default — `echo n | supabase branches create` cancels
// (CLI-1974). Go wraps the branch name in `utils.Aqua` (`create.go:20`).
const yes = yield* legacyResolveYes;
const confirmed = yield* legacyPromptYesNo(
output,
yes,
`Do you want to create a branch named ${legacyAqua(gitBranch.value)}?`,
true,
);
if (!confirmed) {
return yield* new LegacyBranchesCreateCancelledError({ message: CONTEXT_CANCELED_MESSAGE });
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import type { V1CreateABranchOutput } from "@supabase/api/effect";
import { describe, expect, it } from "@effect/vitest";
import { Cause, Effect, Exit, Option } from "effect";
import { Cause, Effect, Exit, Layer, Option } from "effect";
import { Command } from "effect/unstable/cli";

import { mockAnalytics, mockOutput } from "../../../../../tests/helpers/mocks.ts";
import { LEGACY_GLOBAL_FLAGS } from "../../../../shared/legacy/global-flags.ts";
import {
mockAnalytics,
mockOutput,
mockStdin,
mockTty,
} from "../../../../../tests/helpers/mocks.ts";
import { LEGACY_GLOBAL_FLAGS, LegacyYesFlag } from "../../../../shared/legacy/global-flags.ts";
import {
LEGACY_VALID_REF,
buildLegacyTestRuntime,
Expand Down Expand Up @@ -72,6 +77,12 @@ interface SetupOpts {
readonly network?: "fail";
readonly gated?: boolean;
readonly featureKey?: string;
/** Resolved `--yes`/`SUPABASE_YES` for the git-branch auto-name confirm. */
readonly yes?: boolean;
readonly stdinIsTty?: boolean;
/** Piped stdin lines consumed by the non-TTY confirm read. */
readonly stdinInput?: string;
readonly promptConfirmResponses?: ReadonlyArray<boolean>;
}

function buildApiLayer(opts: SetupOpts) {
Expand Down Expand Up @@ -101,17 +112,25 @@ function buildApiLayer(opts: SetupOpts) {
}

function setup(opts: SetupOpts = {}) {
const out = mockOutput({ format: opts.format ?? "text" });
const out = mockOutput({
format: opts.format ?? "text",
promptConfirmResponses: opts.promptConfirmResponses,
});
const analytics = mockAnalytics();
const api = buildApiLayer(opts);
const cliConfig = mockLegacyCliConfig({ workdir: tempRoot.current });
const layer = buildLegacyTestRuntime({
out,
api,
cliConfig,
analytics,
goOutput: opts.goOutput === undefined ? Option.none() : Option.some(opts.goOutput),
});
const layer = Layer.mergeAll(
buildLegacyTestRuntime({
out,
api,
cliConfig,
analytics,
tty: mockTty({ stdinIsTty: opts.stdinIsTty ?? false, stdoutIsTty: false }),
stdin: mockStdin(opts.stdinIsTty ?? false, opts.stdinInput),
goOutput: opts.goOutput === undefined ? Option.none() : Option.some(opts.goOutput),
}),
Layer.succeed(LegacyYesFlag, opts.yes ?? false),
);
return { layer, out, api, analytics };
}

Expand All @@ -122,14 +141,17 @@ function setupTracked(opts: SetupOpts = {}) {
const cliConfig = mockLegacyCliConfig({ workdir: tempRoot.current });
const telemetry = mockLegacyTelemetryStateTracked();
const cache = mockLegacyLinkedProjectCacheTracked();
const layer = buildLegacyTestRuntime({
out,
api,
cliConfig,
analytics,
telemetry: telemetry.layer,
linkedProjectCache: cache.layer,
});
const layer = Layer.mergeAll(
buildLegacyTestRuntime({
out,
api,
cliConfig,
analytics,
telemetry: telemetry.layer,
linkedProjectCache: cache.layer,
}),
Layer.succeed(LegacyYesFlag, opts.yes ?? false),
);
return { layer, out, api, telemetry, cache, analytics };
}

Expand Down Expand Up @@ -197,6 +219,105 @@ describe("legacy branches create integration", () => {
}).pipe(Effect.provide(layer));
});

// ---------------------------------------------------------------------------
// Git-branch auto-name confirmation — Go `create.go:17-28` routes it through
// `PromptYesNo(title, true)` (`console.go:64-82`). `GITHUB_HEAD_REF` drives
// `detectGitBranch` deterministically (its highest-priority source).
// ---------------------------------------------------------------------------

const withGitBranch = <A, E, R>(effect: Effect.Effect<A, E, R>, branch = "feat-y") => {
const prevHead = process.env["GITHUB_HEAD_REF"];
process.env["GITHUB_HEAD_REF"] = branch;
return effect.pipe(
Effect.ensuring(
Effect.sync(() => {
if (prevHead === undefined) delete process.env["GITHUB_HEAD_REF"];
else process.env["GITHUB_HEAD_REF"] = prevHead;
}),
),
);
};

it.live("--yes auto-confirms the git-branch name with the [Y/n] y echo", () => {
const { layer, out, api } = setup({ yes: true, stdinIsTty: true });
return withGitBranch(
Effect.gen(function* () {
yield* legacyBranchesCreate(baseFlags);
// Go's `viper.GetBool("YES")` branch echoes `<title> [Y/n] y` to stderr
// (`console.go:70-72`) instead of blocking the TTY prompt.
expect(out.stderrText).toContain("Do you want to create a branch named ");
expect(out.stderrText).toContain("? [Y/n] y\n");
expect(api.requests[0]?.body).toMatchObject({
branch_name: "feat-y",
git_branch: "feat-y",
});
}).pipe(Effect.provide(layer)),
);
});

it.live("SUPABASE_YES=1 auto-confirms the git-branch name like --yes", () => {
const prev = process.env["SUPABASE_YES"];
process.env["SUPABASE_YES"] = "1";
const { layer, out, api } = setup({ stdinIsTty: true });
return withGitBranch(
Effect.gen(function* () {
yield* legacyBranchesCreate(baseFlags);
expect(out.stderrText).toContain("? [Y/n] y\n");
expect(api.requests[0]?.body).toMatchObject({ branch_name: "feat-y" });
}).pipe(
Effect.ensuring(
Effect.sync(() => {
if (prev === undefined) delete process.env["SUPABASE_YES"];
else process.env["SUPABASE_YES"] = prev;
}),
),
Effect.provide(layer),
),
);
});

it.live("non-TTY with piped `n` declines the git-branch name like Go", () => {
const { layer, out, api } = setup({ stdinIsTty: false, stdinInput: "n\n" });
return withGitBranch(
Effect.gen(function* () {
const exit = yield* Effect.exit(legacyBranchesCreate(baseFlags));
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
expect(JSON.stringify(exit.cause)).toContain("LegacyBranchesCreateCancelledError");
}
// The piped answer is echoed to stderr like Go's non-TTY PromptText.
expect(out.stderrText).toContain("? [Y/n] n\n");
expect(api.requests).toHaveLength(0);
}).pipe(Effect.provide(layer)),
);
});

it.live("non-TTY with empty stdin takes the Yes default and creates the branch", () => {
const { layer, out, api } = setup({ stdinIsTty: false });
return withGitBranch(
Effect.gen(function* () {
yield* legacyBranchesCreate(baseFlags);
// Label printed, empty scan echoed, true default wins (`console.go:64-102`).
expect(out.stderrText).toContain("? [Y/n] \n");
expect(api.requests[0]?.body).toMatchObject({ branch_name: "feat-y" });
}).pipe(Effect.provide(layer)),
);
});

it.live("TTY decline of the git-branch name cancels without creating", () => {
const { layer, api } = setup({ stdinIsTty: true, promptConfirmResponses: [false] });
return withGitBranch(
Effect.gen(function* () {
const exit = yield* Effect.exit(legacyBranchesCreate(baseFlags));
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
expect(JSON.stringify(exit.cause)).toContain("LegacyBranchesCreateCancelledError");
}
expect(api.requests).toHaveLength(0);
}).pipe(Effect.provide(layer)),
);
});

it.live("emits a success event for --output-format=json", () => {
const { layer, out } = setup({ format: "json" });
return Effect.gen(function* () {
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/legacy/commands/config/push/push.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
legacyLoadProjectEnv,
} from "../../../shared/legacy-db-config.toml-read.ts";
import { mapLegacyHttpError } from "../../../shared/legacy-http-errors.ts";
import { legacyPromptYesNo } from "../../../shared/legacy-prompt-yes-no.ts";
import { legacyPromptYesNo } from "../../../../shared/legacy/legacy-prompt-yes-no.ts";
import { legacyCollectDotenvPrivateKeys } from "../../../shared/legacy-vault-decrypt.ts";
import { apiSubsetFromConfig, apiToUpdateBody, diffApiWithRemote } from "./config-sync/api.sync.ts";
import {
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/legacy/commands/db/pull/pull.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { LegacyGoProxy } from "../../../../shared/legacy/go-proxy.service.ts";
import { Output } from "../../../../shared/output/output.service.ts";
import { LegacyCliConfig } from "../../../config/legacy-cli-config.service.ts";
import { legacyAqua, legacyBold } from "../../../shared/legacy-colors.ts";
import { legacyPromptYesNo } from "../../../shared/legacy-prompt-yes-no.ts";
import { legacyPromptYesNo } from "../../../../shared/legacy/legacy-prompt-yes-no.ts";
import {
legacyIpv6Suggestion,
legacyIsIPv6ConnectivityError,
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/legacy/commands/db/push/push.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
legacyApplyMigrations,
legacySeedGlobals,
} from "../../../shared/legacy-migration-apply.ts";
import { legacyPromptYesNo } from "../../../shared/legacy-prompt-yes-no.ts";
import { legacyPromptYesNo } from "../../../../shared/legacy/legacy-prompt-yes-no.ts";
import { legacyToPostgresURL } from "../../../shared/legacy-postgres-url.ts";
import { resolveLegacyDbTargetFlags } from "../../../shared/legacy-db-target-flags.ts";
import { LegacyLinkedProjectCache } from "../../../telemetry/legacy-linked-project-cache.service.ts";
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/legacy/commands/db/reset/reset.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
} from "../../../shared/legacy-db-config.toml-read.ts";
import { LegacyDbConnection } from "../../../shared/legacy-db-connection.service.ts";
import { legacyApplyMigrations } from "../../../shared/legacy-migration-apply.ts";
import { legacyPromptYesNo } from "../../../shared/legacy-prompt-yes-no.ts";
import { legacyPromptYesNo } from "../../../../shared/legacy/legacy-prompt-yes-no.ts";
import {
type LegacyDbConnType,
resolveLegacyDbTargetFlags,
Expand Down
Loading
Loading