diff --git a/apps/cli/src/legacy/commands/db/schema/declarative/sync/SIDE_EFFECTS.md b/apps/cli/src/legacy/commands/db/schema/declarative/sync/SIDE_EFFECTS.md
index e2182911be..ba4ee2562b 100644
--- a/apps/cli/src/legacy/commands/db/schema/declarative/sync/SIDE_EFFECTS.md
+++ b/apps/cli/src/legacy/commands/db/schema/declarative/sync/SIDE_EFFECTS.md
@@ -60,7 +60,10 @@ surfaces before an `--apply`/`--no-apply` conflict is ever checked.
## Output
Text mode only. The generated SQL, the created-migration path, drop-statement
-warnings, and apply status are written to stderr.
+warnings, and apply status are written to stderr. The no-files bootstrap also
+prints `Declarative schema written to
` (the relative declarative dir, Go's
+`GetDeclarativeDir()`) to stderr after generating, writing, and warming the
+catalog cache — on both the interactive-accept and `--yes` paths.
`--no-apply` writes the migration only (never prompts/applies); `--apply` applies
without prompting; both override the global `--yes`. `--no-apply` and `--apply`
are mutually exclusive.
diff --git a/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.handler.ts b/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.handler.ts
index 8ab78569fe..7dda0c611f 100644
--- a/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.handler.ts
+++ b/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.handler.ts
@@ -127,13 +127,15 @@ export const legacyDbSchemaDeclarativeSync = Effect.fn("legacy.db.schema.declara
);
}
+ // Go's `utils.GetDeclarativeDir()` — the config value verbatim (already
+ // `supabase/`-prefixed when relative) or the relative `supabase/database`
+ // default. Printed verbatim in the bootstrap's written-to line below, exactly
+ // as Go prints it (Go chdirs into the workdir, so its paths stay relative).
+ const declarativeDirRel = legacyResolveDeclarativeDir(path, toml.pgDelta);
// `path.resolve` (not `path.join`) so an absolute `declarative_schema_path` is
// used as-is, matching Go's `config.resolve` (which only prefixes the workdir onto
// a relative path). `path.join(workdir, abs)` would mangle the absolute path.
- const declarativeDir = path.resolve(
- cliConfig.workdir,
- legacyResolveDeclarativeDir(path, toml.pgDelta),
- );
+ const declarativeDir = path.resolve(cliConfig.workdir, declarativeDirRel);
const migrationsDir = path.join(cliConfig.workdir, "supabase", "migrations");
const tempDir = legacyPgDeltaTempPath(path, cliConfig.workdir);
const run: LegacyDeclarativeRunContext = {
@@ -249,6 +251,19 @@ export const legacyDbSchemaDeclarativeSync = Effect.fn("legacy.db.schema.declara
if (!run.noCache) {
yield* seam.exportCatalog({ mode: "declarative", noCache: run.noCache });
}
+ // Go's delegated `declarative.Generate` prints the written-to line to stderr
+ // after the write and the catalog warm (`declarative.go:133→138-155→156`), on
+ // both the interactive-accept and --yes/SUPABASE_YES bootstrap paths, and
+ // regardless of --no-cache (the warm is skipped, the line is not). It prints
+ // `utils.GetDeclarativeDir()` — the relative dir above, never a resolved
+ // absolute path, because Go chdirs into the workdir (CLI-1980). NOTE: the
+ // generate and db pull ports of this same Go line still print the absolute
+ // dir today — pull's is moving to relative under CLI-1978; this rendering
+ // is the Go-faithful one.
+ yield* output.raw(
+ `Declarative schema written to ${legacyBold(declarativeDirRel)}\n`,
+ "stderr",
+ );
}
// Step 2: diff migrations state vs declarative; on error, save a debug bundle.
diff --git a/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.integration.test.ts b/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.integration.test.ts
index d4b0df08aa..a5acb0655e 100644
--- a/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.integration.test.ts
+++ b/apps/cli/src/legacy/commands/db/schema/declarative/sync/sync.integration.test.ts
@@ -4,6 +4,7 @@ import { BunServices } from "@effect/platform-bun";
import { describe, expect, it } from "@effect/vitest";
import { Cause, Effect, Exit, Layer, Option } from "effect";
+import { stripAnsi } from "../../../../../../../tests/helpers/ansi.ts";
import { mockOutput, mockStdin, mockTty } from "../../../../../../../tests/helpers/mocks.ts";
import {
mockLegacyCliConfig,
@@ -70,8 +71,16 @@ function setup(workdir: string, opts: SetupOpts = {}) {
const cache = mockLegacyLinkedProjectCacheTracked();
const execInheritCalls: ReadonlyArray[] = [];
const localPostgresImageChecks: Array = [];
+ // Each catalog export records how many raw chunks had been emitted when it fired,
+ // so tests can assert output ordering relative to the exports (e.g. the bootstrap's
+ // written-to line lands after the declarative warm, before the diff's exports).
+ const exportCatalogCalls: Array<{ mode: string; rawChunksAt: number }> = [];
const seam = Layer.succeed(LegacyDeclarativeSeam, {
- exportCatalog: ({ mode }) => Effect.succeed(`supabase/.temp/pgdelta/${mode}.json`),
+ exportCatalog: ({ mode }) =>
+ Effect.sync(() => {
+ exportCatalogCalls.push({ mode, rawChunksAt: out.rawChunks.length });
+ return `supabase/.temp/pgdelta/${mode}.json`;
+ }),
execInherit: (args) =>
Effect.sync(() => {
execInheritCalls.push(args);
@@ -186,7 +195,15 @@ function setup(workdir: string, opts: SetupOpts = {}) {
}),
BunServices.layer,
);
- return { layer, out, execInheritCalls, dbExec, cache, localPostgresImageChecks };
+ return {
+ layer,
+ out,
+ execInheritCalls,
+ dbExec,
+ cache,
+ localPostgresImageChecks,
+ exportCatalogCalls,
+ };
}
const flags = (
@@ -448,6 +465,92 @@ describe("legacy db schema declarative sync integration", () => {
}).pipe(Effect.provide(s.layer));
});
+ it.effect("bootstrap prints the declarative-schema-written line after the catalog warm", () => {
+ // Go's bootstrap delegates to `declarative.Generate`, which prints
+ // `Declarative schema written to ` to stderr AFTER WriteDeclarativeSchemas
+ // and the catalog warm (`declarative.go:133→138-155→156`), before sync's own
+ // diff (step 2). It prints `utils.GetDeclarativeDir()` — the relative
+ // `supabase/database` default — never the absolute resolved dir (CLI-1980).
+ const s = setup(tmp.current, {
+ experimental: true,
+ stdinIsTty: true,
+ diffSql: "",
+ exportJson: EXPORT_JSON,
+ promptConfirmResponses: [true], // generate a new one? yes (no migrations → no reset prompt)
+ });
+ return Effect.gen(function* () {
+ yield* legacyDbSchemaDeclarativeSync(flags({ noApply: Option.some(true) }));
+ const line = `Declarative schema written to ${join("supabase", "database")}\n`;
+ const written = s.out.rawChunks
+ .map((c, index) => ({ text: stripAnsi(c.text), stream: c.stream, index }))
+ .filter((c) => c.text === line);
+ expect(written).toHaveLength(1);
+ expect(written[0]?.stream).toBe("stderr");
+ const lineAt = written[0]?.index ?? -1;
+ // The warm (first declarative-mode export) fires before the line is printed…
+ const warm = s.exportCatalogCalls.find((c) => c.mode === "declarative");
+ expect(warm?.rawChunksAt).toBeLessThanOrEqual(lineAt);
+ // …and the diff's first export (migrations catalog) fires after it, so the
+ // line sits at the end of the bootstrap, matching Go's ordering.
+ const diffStart = s.exportCatalogCalls.find((c) => c.mode === "migrations");
+ expect(diffStart?.rawChunksAt).toBeGreaterThan(lineAt);
+ // The generated files actually landed in the printed (resolved) dir.
+ expect(
+ existsSync(
+ join(tmp.current, "supabase", "database", "schemas", "public", "tables", "players.sql"),
+ ),
+ ).toBe(true);
+ }).pipe(Effect.provide(s.layer));
+ });
+
+ it.effect("--yes bootstrap prints the declarative-schema-written line too", () => {
+ // Go reaches the same delegated `declarative.Generate` print on the
+ // auto-confirmed (--yes / SUPABASE_YES) bootstrap as on the interactive accept.
+ const s = setup(tmp.current, {
+ experimental: true,
+ stdinIsTty: false,
+ yes: true,
+ diffSql: "",
+ exportJson: EXPORT_JSON,
+ });
+ return Effect.gen(function* () {
+ yield* legacyDbSchemaDeclarativeSync(flags({ noApply: Option.some(true) }));
+ expect(
+ s.out.rawChunks.map((c) => ({ text: stripAnsi(c.text), stream: c.stream })),
+ ).toContainEqual({
+ text: `Declarative schema written to ${join("supabase", "database")}\n`,
+ stream: "stderr",
+ });
+ }).pipe(Effect.provide(s.layer));
+ });
+
+ it.effect("--no-cache bootstrap still prints the declarative-schema-written line", () => {
+ // Go's print sits OUTSIDE the `if !noCache` warm gate (`declarative.go:138-156`):
+ // skipping the catalog warm must not skip the line.
+ const s = setup(tmp.current, {
+ experimental: true,
+ stdinIsTty: false,
+ yes: true,
+ diffSql: "",
+ exportJson: EXPORT_JSON,
+ });
+ return Effect.gen(function* () {
+ yield* legacyDbSchemaDeclarativeSync(flags({ noCache: true, noApply: Option.some(true) }));
+ const line = `Declarative schema written to ${join("supabase", "database")}\n`;
+ const written = s.out.rawChunks
+ .map((c, index) => ({ text: stripAnsi(c.text), stream: c.stream, index }))
+ .filter((c) => c.text === line);
+ expect(written).toHaveLength(1);
+ expect(written[0]?.stream).toBe("stderr");
+ // The warm really was skipped: the only declarative-mode export is the diff's,
+ // which fires after the line — yet the line still printed.
+ const lineAt = written[0]?.index ?? -1;
+ const declarativeExports = s.exportCatalogCalls.filter((c) => c.mode === "declarative");
+ expect(declarativeExports).toHaveLength(1);
+ expect(declarativeExports[0]?.rawChunksAt).toBeGreaterThan(lineAt);
+ }).pipe(Effect.provide(s.layer));
+ });
+
it.effect("bootstrap with migrations offers the smart target choice (not local-only)", () => {
// Go delegates the no-files bootstrap to runDeclarativeGenerate; with migrations
// present it offers local/linked/custom rather than silently generating from