Skip to content
Draft
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
2 changes: 1 addition & 1 deletion apps/cli/src/legacy/cli/legacy-complete.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ describe("legacyRespondToComplete", () => {
// `--debug ""` used to return zero candidates entirely: the leftover-args
// computation counted `--debug` itself as "positional leftover," gating
// out subcommand-name completion the way cobra never does for a
// persistent flag: `__complete --debug ''` lists all 36 root commands.
// persistent flag: `__complete --debug ''` lists all root commands.
const result = legacyRespondToComplete(legacyRoot, ["__complete", "--debug", ""]);
expect(result?.directive).toBe(LegacyCompletionDirective.NoFileComp);
expect(result?.candidates.map((c) => c.name)).toContain("branches");
Expand Down
4 changes: 4 additions & 0 deletions apps/cli/src/legacy/cli/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { legacyLinkCommand } from "../commands/link/link.command.ts";
import { legacyLoginCommand } from "../commands/login/login.command.ts";
import { legacyLogoutCommand } from "../commands/logout/logout.command.ts";
import { legacyMigrationCommand } from "../commands/migration/migration.command.ts";
import { legacyMigrationsCommand } from "../commands/migrations/migrations.command.ts";
import { legacySchemaCommand } from "../commands/schema/schema.command.ts";
import { legacyNetworkBansCommand } from "../commands/network-bans/network-bans.command.ts";
import { legacyNetworkRestrictionsCommand } from "../commands/network-restrictions/network-restrictions.command.ts";
import { legacyOrgsCommand } from "../commands/orgs/orgs.command.ts";
Expand Down Expand Up @@ -78,11 +80,13 @@ export const legacyRoot = Command.make("supabase").pipe(
legacyLoginCommand,
legacyLogoutCommand,
legacyMigrationCommand,
legacyMigrationsCommand,
legacyNetworkBansCommand,
legacyNetworkRestrictionsCommand,
legacyOrgsCommand,
legacyPostgresConfigCommand,
legacyProjectsCommand,
legacySchemaCommand,
legacySecretsCommand,
legacySeedCommand,
legacyServicesCommand,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { legacyMigrationFetchCommand } from "./fetch/fetch.command.ts";
export const legacyMigrationCommand = Command.make("migration").pipe(
Command.withDescription("Manage database migration scripts."),
Command.withShortDescription("Manage database migration scripts"),
Command.withAlias("migrations"),
Command.withSubcommands([
legacyMigrationListCommand,
legacyMigrationNewCommand,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,48 @@ import { CliOutput, Command } from "effect/unstable/cli";
import { textCliOutputFormatter } from "../../../shared/output/text-formatter.ts";
import { LEGACY_GLOBAL_FLAGS } from "../../../shared/legacy/global-flags.ts";
import { legacyMigrationCommand } from "./migration.command.ts";
import { legacyMigrationsCommand } from "../migrations/migrations.command.ts";

// `withGlobalFlags` must come AFTER `withSubcommands` — see
// `start.string-slice-flags.integration.test.ts`'s identical comment.
const legacyTestRoot = Command.make("supabase").pipe(
Command.withSubcommands([legacyMigrationCommand]),
Command.withSubcommands([legacyMigrationCommand, legacyMigrationsCommand]),
Command.withGlobalFlags(LEGACY_GLOBAL_FLAGS),
);

describe("legacy migration command integration", () => {
it.live("accepts the Go-compatible plural migrations alias", () => {
// After CLI-1969, `squash` is native and no `migration` subcommand is proxied
// any more — so the plural alias is now proven at the PARSER instead: a
// `migrations squash --nope` must fail with squash's own unknown-flag error,
// which never builds the command's `Command.provide` runtime layer.
describe("legacy migration and migrations commands", () => {
it.live("keeps singular migration as the Go-parity group", () => {
const run = Effect.gen(function* () {
const exit = yield* Command.runWith(legacyTestRoot, { version: "0.0.0-test" })([
"migrations",
"migration",
"squash",
"--nope",
]).pipe(Effect.exit);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const causeJson = JSON.stringify(exit.cause);
// The alias resolved: the parse error is scoped to the squash LEAF, not the root.
expect(causeJson).toContain('"commandPath":["supabase","migration","squash"]');
expect(causeJson).not.toContain('"subcommand":"migrations"');
}
}).pipe(Effect.provide(CliOutput.layer(textCliOutputFormatter())));

// Command.runWith's Environment type is retained even though this path only needs CliOutput
// at runtime.
return run as Effect.Effect<void>;
});

it.live("routes plural migrations to the schema-first group", () => {
const run = Effect.gen(function* () {
const exit = yield* Command.runWith(legacyTestRoot, { version: "0.0.0-test" })([
"migrations",
"apply",
"--nope",
]).pipe(Effect.exit);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const causeJson = JSON.stringify(exit.cause);
expect(causeJson).toContain('"commandPath":["supabase","migrations","apply"]');
expect(causeJson).not.toContain('"commandPath":["supabase","migration","apply"]');
}
}).pipe(Effect.provide(CliOutput.layer(textCliOutputFormatter())));

return run as Effect.Effect<void>;
});
});
14 changes: 14 additions & 0 deletions apps/cli/src/legacy/commands/migrations/apply/apply.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Command } from "effect/unstable/cli";
import { withJsonErrorHandling } from "../../../../shared/output/json-error-handling.ts";
import { withLegacyCommandInstrumentation } from "../../../telemetry/legacy-command-instrumentation.ts";
import { legacySchemaRuntimeLayer } from "../../../schema/legacy-schema-runtime.layer.ts";
import { legacyMigrationsApply } from "./apply.handler.ts";

export const legacyMigrationsApplyCommand = Command.make("apply").pipe(
Command.withDescription("Apply exact pending migration files to the local database."),
Command.withShortDescription("Apply pending migrations locally"),
Command.withHandler(() =>
legacyMigrationsApply().pipe(withLegacyCommandInstrumentation(), withJsonErrorHandling),
),
Command.provide(legacySchemaRuntimeLayer(["migrations", "apply"])),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Effect } from "effect";
import { applyMigrations } from "../../../../shared/migrations/apply-migrations.ts";
import { renderSchemaResult } from "../../../../shared/schema/schema-render.ts";

export const legacyMigrationsApply = Effect.fn("legacy.migrations.apply")(function* () {
const result = yield* applyMigrations();
yield* renderSchemaResult("Apply migrations", result);
});
39 changes: 39 additions & 0 deletions apps/cli/src/legacy/commands/migrations/diff/diff.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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 { withLegacyCommandInstrumentation } from "../../../telemetry/legacy-command-instrumentation.ts";
import { legacySchemaRuntimeLayer } from "../../../schema/legacy-schema-runtime.layer.ts";
import { legacyMigrationsDiff } from "./diff.handler.ts";

const config = {
against: Flag.string("against").pipe(
Flag.withDescription("Live database to compare: local, linked, or a connection string."),
Flag.optional,
),
file: Flag.string("file").pipe(
Flag.withDescription("Write preview SQL to a file without applying it."),
Flag.withAlias("f"),
Flag.optional,
),
} as const;

export type LegacyMigrationsDiffFlags = CliCommand.Command.Config.Infer<typeof config>;

export const legacyMigrationsDiffCommand = Command.make("diff", config).pipe(
Command.withDescription(
"Preview the SQL required to move from migration replay to a live database.\n\n" +
"This is the successor to db diff. It never mutates the database.",
),
Command.withShortDescription("Diff migration replay against a live database"),
Command.withExamples([
{ command: "supabase migrations diff --against local", description: "Preview local drift" },
{ command: "supabase migrations diff --against linked", description: "Preview remote drift" },
]),
Command.withHandler((flags) =>
legacyMigrationsDiff(flags).pipe(
withLegacyCommandInstrumentation({ flags, config, aliases: { f: "file" } }),
withJsonErrorHandling,
),
),
Command.provide(legacySchemaRuntimeLayer(["migrations", "diff"])),
);
14 changes: 14 additions & 0 deletions apps/cli/src/legacy/commands/migrations/diff/diff.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Effect, Option } from "effect";
import { diffMigrations } from "../../../../shared/migrations/diff-migrations.ts";
import { renderSchemaResult } from "../../../../shared/schema/schema-render.ts";
import type { LegacyMigrationsDiffFlags } from "./diff.command.ts";

export const legacyMigrationsDiff = Effect.fn("legacy.migrations.diff")(function* (
flags: LegacyMigrationsDiffFlags,
) {
const result = yield* diffMigrations({
against: Option.getOrUndefined(flags.against),
file: Option.getOrUndefined(flags.file),
});
yield* renderSchemaResult("Diff migrations", result);
});
27 changes: 27 additions & 0 deletions apps/cli/src/legacy/commands/migrations/list/list.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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 { withLegacyCommandInstrumentation } from "../../../telemetry/legacy-command-instrumentation.ts";
import { legacySchemaRuntimeLayer } from "../../../schema/legacy-schema-runtime.layer.ts";
import { legacyMigrationsList } from "./list.handler.ts";

const config = {
against: Flag.string("against").pipe(
Flag.withDescription("Target to compare: local, linked, or a connection string."),
Flag.optional,
),
} as const;

export type LegacyMigrationsListFlags = CliCommand.Command.Config.Infer<typeof config>;

export const legacyMigrationsListCommand = Command.make("list", config).pipe(
Command.withDescription("Compare local migration files with target migration history."),
Command.withShortDescription("List local and remote migrations"),
Command.withHandler((flags) =>
legacyMigrationsList(flags).pipe(
withLegacyCommandInstrumentation({ flags, config }),
withJsonErrorHandling,
),
),
Command.provide(legacySchemaRuntimeLayer(["migrations", "list"])),
);
11 changes: 11 additions & 0 deletions apps/cli/src/legacy/commands/migrations/list/list.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Effect, Option } from "effect";
import { listMigrations } from "../../../../shared/migrations/list-migrations.ts";
import { renderSchemaResult } from "../../../../shared/schema/schema-render.ts";
import type { LegacyMigrationsListFlags } from "./list.command.ts";

export const legacyMigrationsList = Effect.fn("legacy.migrations.list")(function* (
flags: LegacyMigrationsListFlags,
) {
const result = yield* listMigrations({ against: Option.getOrUndefined(flags.against) });
yield* renderSchemaResult("List migrations", result);
});
26 changes: 26 additions & 0 deletions apps/cli/src/legacy/commands/migrations/migrations.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Command } from "effect/unstable/cli";
import { SCHEMA_ECOSYSTEM_MAPPING_HELP } from "../../../shared/schema/schema-ecosystem.ts";
import { legacyMigrationsApplyCommand } from "./apply/apply.command.ts";
import { legacyMigrationsDiffCommand } from "./diff/diff.command.ts";
import { legacyMigrationsListCommand } from "./list/list.command.ts";
import { legacyMigrationsNewCommand } from "./new/new.command.ts";
import { legacyMigrationsPullCommand } from "./pull/pull.command.ts";
import { legacyMigrationsPushCommand } from "./push/push.command.ts";

export const legacyMigrationsCommand = Command.make("migrations").pipe(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add the required legacy side-effect manifests

Neither new stable-shell command group includes a SIDE_EFFECTS.md, so the database writes, filesystem changes, environment inputs, and failure exits introduced by these commands have no compatibility checklist. Add the required manifest for both schema and migrations rather than shipping undocumented legacy command surfaces.

AGENTS.md reference: apps/cli/AGENTS.md:L185-L189

Useful? React with 👍 / 👎.

Command.withDescription(
"Advanced file-and-history database workflow.\n\n" +
"These commands operate on supabase/migrations and do not load declarative SQL. " +
"migrations push is the only path that mutates a durable remote schema.\n\n" +
SCHEMA_ECOSYSTEM_MAPPING_HELP,
),
Command.withShortDescription("Manage migration files and history"),
Command.withSubcommands([
legacyMigrationsNewCommand,
legacyMigrationsListCommand,
legacyMigrationsDiffCommand,
legacyMigrationsApplyCommand,
legacyMigrationsPushCommand,
legacyMigrationsPullCommand,
]),
);
30 changes: 30 additions & 0 deletions apps/cli/src/legacy/commands/migrations/new/new.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Argument, Command } from "effect/unstable/cli";
import type * as CliCommand from "effect/unstable/cli/Command";
import { withJsonErrorHandling } from "../../../../shared/output/json-error-handling.ts";
import { withLegacyCommandInstrumentation } from "../../../telemetry/legacy-command-instrumentation.ts";
import { legacySchemaRuntimeLayer } from "../../../schema/legacy-schema-runtime.layer.ts";
import { legacyMigrationsNew } from "./new.handler.ts";

const config = {
name: Argument.string("name").pipe(
Argument.withDescription("Migration name."),
Argument.optional,
),
} as const;

export type LegacyMigrationsNewFlags = CliCommand.Command.Config.Infer<typeof config>;

export const legacyMigrationsNewCommand = Command.make("new", config).pipe(
Command.withDescription("Create an empty migration file for manual authoring."),
Command.withShortDescription("Create an empty migration"),
Command.withExamples([
{
command: "supabase migrations new add_custom_data",
description: "Create supabase/migrations/<timestamp>_add_custom_data.sql",
},
]),
Command.withHandler((flags) =>
legacyMigrationsNew(flags).pipe(withLegacyCommandInstrumentation(), withJsonErrorHandling),
),
Command.provide(legacySchemaRuntimeLayer(["migrations", "new"])),
);
11 changes: 11 additions & 0 deletions apps/cli/src/legacy/commands/migrations/new/new.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Effect, Option } from "effect";
import { newMigration } from "../../../../shared/migrations/new-migration.ts";
import { renderSchemaResult } from "../../../../shared/schema/schema-render.ts";
import type { LegacyMigrationsNewFlags } from "./new.command.ts";

export const legacyMigrationsNew = Effect.fn("legacy.migrations.new")(function* (
flags: LegacyMigrationsNewFlags,
) {
const result = yield* newMigration(Option.getOrUndefined(flags.name));
yield* renderSchemaResult("Create migration", result);
});
34 changes: 34 additions & 0 deletions apps/cli/src/legacy/commands/migrations/pull/pull.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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 { withLegacyCommandInstrumentation } from "../../../telemetry/legacy-command-instrumentation.ts";
import { legacySchemaRuntimeLayer } from "../../../schema/legacy-schema-runtime.layer.ts";
import { legacyMigrationsPull } from "./pull.handler.ts";

const config = {
from: Flag.string("from").pipe(
Flag.withDescription("Remote database: linked or a connection string."),
Flag.optional,
),
name: Flag.string("name").pipe(
Flag.withDescription("Name for the pulled migration file."),
Flag.optional,
),
} as const;

export type LegacyMigrationsPullFlags = CliCommand.Command.Config.Infer<typeof config>;

export const legacyMigrationsPullCommand = Command.make("pull", config).pipe(
Command.withDescription(
"Record remote-only database state as local migration files.\n\n" +
"Does not interpret declarative SQL.",
),
Command.withShortDescription("Pull remote schema drift into migrations"),
Command.withHandler((flags) =>
legacyMigrationsPull(flags).pipe(
withLegacyCommandInstrumentation({ flags, config }),
withJsonErrorHandling,
),
),
Command.provide(legacySchemaRuntimeLayer(["migrations", "pull"])),
);
14 changes: 14 additions & 0 deletions apps/cli/src/legacy/commands/migrations/pull/pull.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Effect, Option } from "effect";
import { pullMigrations } from "../../../../shared/migrations/pull-migrations.ts";
import { renderSchemaResult } from "../../../../shared/schema/schema-render.ts";
import type { LegacyMigrationsPullFlags } from "./pull.command.ts";

export const legacyMigrationsPull = Effect.fn("legacy.migrations.pull")(function* (
flags: LegacyMigrationsPullFlags,
) {
const result = yield* pullMigrations({
from: Option.getOrUndefined(flags.from),
name: Option.getOrUndefined(flags.name),
});
yield* renderSchemaResult("Pull remote migrations", result);
});
45 changes: 45 additions & 0 deletions apps/cli/src/legacy/commands/migrations/push/push.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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 { withLegacyCommandInstrumentation } from "../../../telemetry/legacy-command-instrumentation.ts";
import { legacySchemaRuntimeLayer } from "../../../schema/legacy-schema-runtime.layer.ts";
import { legacyMigrationsPush } from "./push.handler.ts";

const config = {
yes: Flag.boolean("yes").pipe(
Flag.withDescription("Answer ordinary prompts. Does not skip target identity or live verify."),
Flag.withAlias("y"),
),
Comment on lines +9 to +12

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Consume the existing global --yes flag

For the supported persistent-flag form supabase --yes migrations push, the root consumes the global LegacyYesFlag, while this same-named local flag remains false and is the value passed to authorizeMutation; a non-interactive linked push is therefore rejected despite explicit confirmation. Reuse or merge the root global flag rather than shadowing it locally.

AGENTS.md reference: apps/cli/AGENTS.md:L340-L344

Useful? React with 👍 / 👎.

projectRef: Flag.string("project-ref").pipe(
Flag.withDescription("Must match the resolved linked project."),
Flag.optional,
),
allowRemote: Flag.boolean("allow-remote").pipe(
Flag.withDescription("Acknowledge an unverifiable --db-url target."),
),
dbUrl: Flag.string("db-url").pipe(
Flag.withDescription("Raw connection string. Requires --allow-remote."),
Flag.optional,
),
skipVerify: Flag.boolean("skip-verify").pipe(
Flag.withDescription("Skip isolated-shadow declarations-ahead and remote-drift checks."),
),
} as const;

export type LegacyMigrationsPushFlags = CliCommand.Command.Config.Infer<typeof config>;

export const legacyMigrationsPushCommand = Command.make("push", config).pipe(
Command.withDescription(
"Apply exact pending migration files to the linked platform database.\n\n" +
"This is the only CLI path that mutates durable remote schema. " +
"It fails closed when declarations are ahead of the migration head or remote drift is detected.",
),
Command.withShortDescription("Push pending migrations to the platform"),
Command.withHandler((flags) =>
legacyMigrationsPush(flags).pipe(
withLegacyCommandInstrumentation({ flags, config, aliases: { y: "yes" } }),
withJsonErrorHandling,
),
),
Command.provide(legacySchemaRuntimeLayer(["migrations", "push"])),
);
Loading
Loading