Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
adf4cd6
fix(cli): port db pull initial pull to native pg_dump; fix non-intera…
Coly010 Jun 29, 2026
af5e58e
fix(db): reset dump byte tracking per attempt (Go resetOutput parity)
Coly010 Jun 30, 2026
2bfb3f8
fix(cli): honor piped stdin in legacy yes/no prompts (Go console parity)
Coly010 Jun 30, 2026
90f04f6
fix(cli): read legacy prompt stdin lazily, one line at a time (Go Rea…
Coly010 Jun 30, 2026
a71f30c
fix(cli): honor piped logout confirmations (Go console parity)
Coly010 Jun 30, 2026
af190ce
Merge remote-tracking branch 'origin/develop' into cli/db-pull-native…
Coly010 Jun 30, 2026
acc9614
fix(db): honor SUPABASE_YES and drop `as` casts on the native db pull…
Coly010 Jun 30, 2026
4325294
test(db): gate db dump/pull live suites on data-plane readiness
Coly010 Jul 1, 2026
b2db027
fix(cli): honor SUPABASE_YES for logout confirmation
Coly010 Jul 1, 2026
79aa8d0
fix(db): honor project .env SUPABASE_YES for db pull; stop live test …
Coly010 Jul 1, 2026
6ef45e5
fix(cli): apply project .env to process.env so the registry mirror is…
Coly010 Jul 1, 2026
3673599
fix(cli): honor project .env SUPABASE_YES for config push confirmations
Coly010 Jul 1, 2026
060f02c
fix(cli): keep project-ref identity keys out of the project .env proc…
Coly010 Jul 1, 2026
3076b58
fix(cli): narrow the project .env process.env apply to an allowlist (…
Coly010 Jul 1, 2026
3223e09
fix(cli): load config push env from the resolved project root
Coly010 Jul 1, 2026
e69a5bd
fix(cli): honor explicit --yes=false over SUPABASE_YES (Go bound-pfla…
Coly010 Jul 1, 2026
9a7ef6f
test(db): require success when the live pull writes a migration
Coly010 Jul 1, 2026
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
300 changes: 150 additions & 150 deletions apps/cli/docs/go-cli-porting-status.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
LegacyOutputFlag,
} from "../../../shared/legacy/global-flags.ts";
import { LegacyGoProxy } from "../../../shared/legacy/go-proxy.service.ts";
import { CliArgs } from "../../../shared/cli/cli-args.service.ts";
import { LegacyTemplateService, type LegacyStarterTemplate } from "./bootstrap.templates.ts";
import { legacyBootstrap } from "./bootstrap.handler.ts";
import type { LegacyBootstrapFlags } from "./bootstrap.command.ts";
Expand Down Expand Up @@ -175,6 +176,7 @@ function setup(opts: SetupOpts = {}) {
Layer.succeed(LegacyWorkdirFlag, opts.workdir ?? Option.some(tempRoot.current)),
Layer.succeed(LegacyYesFlag, opts.yes ?? false),
Layer.succeed(LegacyDebugFlag, opts.debug ?? false),
Layer.succeed(CliArgs, { args: [] }),
);

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
LegacyYesFlag,
} from "../../../shared/legacy/global-flags.ts";
import { LegacyGoProxy } from "../../../shared/legacy/go-proxy.service.ts";
import { CliArgs } from "../../../shared/cli/cli-args.service.ts";
import { legacyDebugLoggerLayer } from "../../shared/legacy-debug-logger.layer.ts";
import { legacyIdentityStitchLayer } from "../../shared/legacy-identity-stitch.ts";
import { legacyCliConfigLayer } from "../../config/legacy-cli-config.layer.ts";
Expand Down Expand Up @@ -117,6 +118,7 @@ describe("legacy bootstrap linked-project cache location", () => {
Layer.succeed(LegacyYesFlag, false),
Layer.succeed(LegacyOutputFlag, Option.none()),
Layer.succeed(LegacyDebugFlag, false),
Layer.succeed(CliArgs, { args: [] }),
);
const runtime = mockRuntimeInfo({ cwd: parent });
const credentials = mockLegacyCredentialsTracked();
Expand Down
9 changes: 8 additions & 1 deletion apps/cli/src/legacy/commands/config/push/push.command.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Layer } from "effect";
import { Command, Flag } from "effect/unstable/cli";
import type * as CliCommand from "effect/unstable/cli/Command";

import { withJsonErrorHandling } from "../../../../shared/output/json-error-handling.ts";
import { legacyManagementApiRuntimeLayer } from "../../../shared/legacy-management-api-runtime.layer.ts";
import { legacyPromptInputRuntimeLayer } from "../../../shared/legacy-prompt-input.layer.ts";
import { withLegacyCommandInstrumentation } from "../../../telemetry/legacy-command-instrumentation.ts";
import { legacyConfigPush } from "./push.handler.ts";

Expand Down Expand Up @@ -34,5 +36,10 @@ export const legacyConfigPushCommand = Command.make("push", config).pipe(
withJsonErrorHandling,
),
),
Command.provide(legacyManagementApiRuntimeLayer(["config", "push"])),
Command.provide(
Layer.mergeAll(
legacyManagementApiRuntimeLayer(["config", "push"]),
legacyPromptInputRuntimeLayer,
),
),
);
39 changes: 22 additions & 17 deletions apps/cli/src/legacy/commands/config/push/push.handler.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { loadProjectConfig } from "@supabase/config";
import { Effect } from "effect";
import { findProjectRoot, loadProjectConfig } from "@supabase/config";
import { Effect, FileSystem, Path } from "effect";

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 { legacyResolveYes } from "../../../../shared/legacy/global-flags.ts";
import { legacyResolveYesWithProjectEnv } from "../../../../shared/legacy/global-flags.ts";
import { Output } from "../../../../shared/output/output.service.ts";
import { RuntimeInfo } from "../../../../shared/runtime/runtime-info.service.ts";
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 { apiSubsetFromConfig, apiToUpdateBody, diffApiWithRemote } from "./config-sync/api.sync.ts";
import {
applyRemoteAuthConfig,
Expand Down Expand Up @@ -87,8 +89,18 @@ export const legacyConfigPush = Effect.fn("legacy.config.push")(function* (
const linkedProjectCache = yield* LegacyLinkedProjectCache;
const telemetryState = yield* LegacyTelemetryState;
const runtimeInfo = yield* RuntimeInfo;
// `--yes` OR `SUPABASE_YES` (Go's viper AutomaticEnv, root.go:318-320).
const yes = yield* legacyResolveYes;
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
// `--yes` OR `SUPABASE_YES` (Go's viper AutomaticEnv, root.go:318-320). Go's
// `config push` runs `flags.LoadConfig`, which imports `supabase/.env` before
// `PromptYesNo` reads `viper.GetBool("YES")`, so a `SUPABASE_YES` set only in
// `supabase/.env` auto-confirms. Resolve against the project env, not just the
// flag + shell env. Load it from the resolved project root (walking up, same as
// `loadProjectConfig` below and Go's `ChangeWorkDir` before `LoadConfig`), so a
// push from a subdirectory still reads the project root's `supabase/.env`.
const projectRoot = (yield* findProjectRoot(runtimeInfo.cwd)) ?? runtimeInfo.cwd;
const projectEnv = yield* legacyLoadProjectEnv(fs, path, projectRoot);
const yes = yield* legacyResolveYesWithProjectEnv(projectEnv);

const ref = yield* resolver.resolve(flags.projectRef);

Expand Down Expand Up @@ -154,24 +166,17 @@ export const legacyConfigPush = Effect.fn("legacy.config.push")(function* (

yield* output.raw(`Pushing config to project: ${projectId}\n`, "stderr");

// keep(name): Go push.go `keep` + console.PromptYesNo(title, true).
const keep = (name: string): Effect.Effect<boolean> =>
// keep(name): Go push.go `keep` + console.PromptYesNo(title, true). The shared
// helper mirrors Go's prompt across all modes, including scanning piped stdin on
// a non-TTY before falling back to the default (`console.go:64-82`).
const keep = (name: string) =>
Effect.gen(function* () {
const item = cost.get(name);
const title =
item === undefined
? `Do you want to push ${name} config to remote?`
: `Enabling ${item.name} will cost you ${item.price}. Keep it enabled?`;
if (output.format !== "text") {
return true;
}
if (yes) {
yield* output.raw(`${title} [Y/n] y\n`, "stderr");
return true;
}
return yield* output
.promptConfirm(title, { defaultValue: true })
.pipe(Effect.orElseSucceed(() => true));
return yield* legacyPromptYesNo(output, yes, title, true);
Comment thread
Coly010 marked this conversation as resolved.
});

const services: Array<LegacyConfigPushServiceResult> = [];
Expand Down
119 changes: 114 additions & 5 deletions apps/cli/src/legacy/commands/config/push/push.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import {
mockLegacyTelemetryStateTracked,
useLegacyTempWorkdir,
} from "../../../../../tests/helpers/legacy-mocks.ts";
import { mockRuntimeInfo } from "../../../../../tests/helpers/mocks.ts";
import { mockRuntimeInfo, mockTty } from "../../../../../tests/helpers/mocks.ts";
import { mockLegacyPromptInput } from "../../../../../tests/helpers/legacy-prompt-input.ts";
import { LegacyYesFlag } from "../../../../shared/legacy/global-flags.ts";
import { legacyConfigPush } from "./push.handler.ts";

Expand Down Expand Up @@ -57,6 +58,12 @@ function setup(opts: {
readonly yes?: boolean;
readonly confirm?: ReadonlyArray<boolean>;
readonly promptFail?: boolean;
/** stdin interactivity; defaults to a TTY so prompt-driven tests reach the confirm. */
readonly stdinIsTty?: boolean;
/** Piped (non-TTY) stdin answers, one consumed per confirmation prompt. */
readonly pipedAnswers?: ReadonlyArray<string>;
/** Working directory the handler runs from; defaults to the temp project root. */
readonly runtimeCwd?: string;
}) {
writeConfig(opts.toml);
const routes = opts.routes ?? {};
Expand Down Expand Up @@ -111,10 +118,12 @@ function setup(opts: {
out,
api,
cliConfig: mockLegacyCliConfig({ workdir: tempRoot.current }),
runtimeInfo: mockRuntimeInfo({ cwd: tempRoot.current }),
runtimeInfo: mockRuntimeInfo({ cwd: opts.runtimeCwd ?? tempRoot.current }),
telemetry: telemetry.layer,
linkedProjectCache: linkedProjectCache.layer,
tty: mockTty({ stdinIsTty: opts.stdinIsTty ?? true, stdoutIsTty: false }),
}),
mockLegacyPromptInput({ pipedLines: opts.pipedAnswers }),
Layer.succeed(LegacyYesFlag, opts.yes ?? false),
);
return { layer, out, api, telemetry, linkedProjectCache };
Expand Down Expand Up @@ -296,10 +305,13 @@ project_id = "abcdefghijklmnopqrst"
}).pipe(Effect.provide(layer));
});

it.live("defaults to yes in non-TTY text without --yes", () => {
const { layer, api } = setup({
it.live("defaults to yes on empty non-TTY stdin, echoing the prompt", () => {
// Go's `PromptYesNo(..., true)` (`push.go:36`) prints the label and scans
// stdin even on a non-terminal (`console.go:96-102`); with no piped input the
// scan is empty and it falls back to the default (`true`), so the push proceeds.
const { layer, api, out } = setup({
toml: API_ONLY_TOML,
promptFail: true,
stdinIsTty: false,
routes: {
postgrestGet: { status: 200, body: POSTGREST_DISABLED },
postgresGet: { status: 200, body: {} },
Expand All @@ -310,9 +322,102 @@ project_id = "abcdefghijklmnopqrst"
expect(api.requests.some((r) => r.method === "PATCH" && r.url.includes("/postgrest"))).toBe(
true,
);
// Label printed + empty answer echoed (Go's non-TTY `PromptText`).
expect(out.stderrText).toContain("Do you want to push api config to remote? [Y/n] \n");
}).pipe(Effect.provide(layer));
});

it.live("honors a piped 'n' decline on non-TTY stdin (no update)", () => {
// Regression: Go scans piped stdin before defaulting (`console.go:74-82`), so a
// piped `n` cancels the push even on a non-terminal — it must not silently apply.
const { layer, api, out } = setup({
toml: API_ONLY_TOML,
stdinIsTty: false,
pipedAnswers: ["n"],
routes: {
postgrestGet: { status: 200, body: POSTGREST_DISABLED },
postgresGet: { status: 200, body: {} },
},
});
return Effect.gen(function* () {
yield* legacyConfigPush({ projectRef: Option.none() });
expect(api.requests.some((r) => r.method === "PATCH" && r.url.includes("/postgrest"))).toBe(
false,
);
// The consumed answer is echoed to stderr (Go's non-TTY `PromptText`).
expect(out.stderrText).toContain("Do you want to push api config to remote? [Y/n] n");
}).pipe(Effect.provide(layer));
});

it.live("honors SUPABASE_YES from supabase/.env even against a piped 'n'", () => {
// Go's config push runs `flags.LoadConfig`, importing `supabase/.env` before
// `PromptYesNo`, so a project-local `SUPABASE_YES=true` auto-confirms before
// stdin is read — the push proceeds despite the piped `n`.
const prev = process.env["SUPABASE_YES"];
delete process.env["SUPABASE_YES"];
const { layer, api } = setup({
toml: API_ONLY_TOML,
stdinIsTty: false,
pipedAnswers: ["n"],
routes: {
postgrestGet: { status: 200, body: POSTGREST_DISABLED },
postgresGet: { status: 200, body: {} },
},
});
// Written after setup()'s writeConfig created supabase/.
writeFileSync(join(tempRoot.current, "supabase", ".env"), "SUPABASE_YES=true\n");
return Effect.gen(function* () {
yield* legacyConfigPush({ projectRef: Option.none() });
expect(api.requests.some((r) => r.method === "PATCH" && r.url.includes("/postgrest"))).toBe(
true,
);
}).pipe(
Effect.ensuring(
Effect.sync(() => {
if (prev === undefined) delete process.env["SUPABASE_YES"];
else process.env["SUPABASE_YES"] = prev;
}),
),
Effect.provide(layer),
);
});

it.live("loads config-push env from the project root when run from a subdirectory", () => {
// Go's ChangeWorkDir moves to the project root before flags.LoadConfig, so a
// SUPABASE_YES in <root>/supabase/.env auto-confirms even when invoked from a
// subdir. The env load must walk up like loadProjectConfig, not use the raw cwd.
const prev = process.env["SUPABASE_YES"];
delete process.env["SUPABASE_YES"];
const sub = join(tempRoot.current, "nested", "dir");
mkdirSync(sub, { recursive: true });
const { layer, api } = setup({
toml: API_ONLY_TOML,
stdinIsTty: false,
pipedAnswers: ["n"],
runtimeCwd: sub,
routes: {
postgrestGet: { status: 200, body: POSTGREST_DISABLED },
postgresGet: { status: 200, body: {} },
},
});
// `.env` lives at the project ROOT (setup's writeConfig wrote config.toml there).
writeFileSync(join(tempRoot.current, "supabase", ".env"), "SUPABASE_YES=true\n");
return Effect.gen(function* () {
yield* legacyConfigPush({ projectRef: Option.none() });
expect(api.requests.some((r) => r.method === "PATCH" && r.url.includes("/postgrest"))).toBe(
true,
);
}).pipe(
Effect.ensuring(
Effect.sync(() => {
if (prev === undefined) delete process.env["SUPABASE_YES"];
else process.env["SUPABASE_YES"] = prev;
}),
),
Effect.provide(layer),
);
});

it.live("emits a structured summary in json mode without prompts", () => {
const { layer, out } = setup({
toml: API_ONLY_TOML,
Expand Down Expand Up @@ -394,6 +499,7 @@ file_size_limit = "50MiB"
cliConfig: mockLegacyCliConfig({ workdir: tempRoot.current }),
runtimeInfo: mockRuntimeInfo({ cwd: tempRoot.current }),
}),
mockLegacyPromptInput(),
Layer.succeed(LegacyYesFlag, true),
);
return Effect.gen(function* () {
Expand Down Expand Up @@ -462,7 +568,10 @@ function setupService(opts: {
runtimeInfo: mockRuntimeInfo({ cwd: opts.runtimeCwd ?? tempRoot.current }),
telemetry: telemetry.layer,
linkedProjectCache: linkedProjectCache.layer,
// Gated-service prompts model an interactive user answering via `confirm`.
tty: mockTty({ stdinIsTty: true, stdoutIsTty: false }),
}),
mockLegacyPromptInput(),
Layer.succeed(LegacyYesFlag, opts.yes ?? false),
);
return { layer, out, apiMock };
Expand Down
Loading
Loading