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
4 changes: 2 additions & 2 deletions apps/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,11 @@ The CLI is built on `effect/unstable/cli`.

Important areas:

- `src/shared/cli/` for shared runner logic, roots, and global flags
- `src/shared/cli/` for shared runner logic and roots
- `src/commands/` for the command tree
- `src/shared/output/` for text / JSON / NDJSON output policies
- `src/shared/runtime/` for TTY, stdin, browser, and process-control services
- `src/shared/auth/` for login-related services
- `src/auth/` for credential and platform API services; `src/shared/auth/` keeps the JWKS and keyring helpers

The local stack commands use `@supabase/stack` for lifecycle, status, logs, and runtime operations.
Managed ownership uses stable loopback `GET /owner` and session-fenced `POST /stop`; same-version
Expand Down
12 changes: 6 additions & 6 deletions apps/cli/docs/code-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ Shared concern files:

```text
auth/
credentials.service.ts
credentials.layer.ts
command-credentials.service.ts
command-credentials.layer.ts
errors.ts
```

Expand All @@ -67,12 +67,12 @@ Rules:
- Shared layer files use `<topic>.layer.ts`.
- Do not prefix leaf files with the slice name.
- `tracing.layer.ts`, not `telemetry.tracing.layer.ts`
- `credentials.service.ts`, not `auth.credentials.service.ts`
- `command-credentials.service.ts`, not `auth.command-credentials.service.ts`

## Symbol Naming

- Service symbols are plain nouns: `Credentials`, `Tracing`, `Output`.
- Layer exports use concrete `*Layer` names: `credentialsLayer`, `tracingLayer`, `outputLayer`.
- Service symbols are plain nouns: `Tty`, `Analytics`, `Output`.
- Layer exports use concrete `*Layer` names: `ttyLayer`, `analyticsLayer`, `tracingLayer`.
- Do not use `.Default`.
- Do not rely on `static layer` as the default pattern for shared concern slices.

Expand Down Expand Up @@ -136,6 +136,6 @@ Examples where comments are expected:

- `telemetry/tracing.layer.ts`
- `output/output.layer.ts`
- `auth/credentials.layer.ts`
- `auth/command-credentials.layer.ts`

Consistency does not mean every service or layer file needs a header. The goal is high-signal comments on important boundaries.
5 changes: 3 additions & 2 deletions apps/cli/docs/tracing-monitoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ Tracing answers observability questions such as:
This path is span-based. It is intentionally separate from PostHog analytics, which is event-based
and optimized for product questions rather than operational traces.

The tracing implementation is currently owned by the `Tracing` service and
[`src/shared/telemetry/tracing.layer.ts`](../src/shared/telemetry/tracing.layer.ts).
The tracing implementation lives in
[`src/shared/telemetry/tracing.layer.ts`](../src/shared/telemetry/tracing.layer.ts), which provides
Effect's `Tracer` service.

## What Happens Today

Expand Down
18 changes: 13 additions & 5 deletions apps/cli/src/cli/complete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ import {
PropExitCode,
PropOutputFormat,
} from "../shared/telemetry/event-catalog.ts";
import { standaloneAnalyticsConfigLayer } from "../shared/telemetry/standalone-analytics-config.layer.ts";
import { cliProjectContextLayer } from "../shared/config/cli-project-context.layer.ts";
import { cliSettingsLayer } from "../shared/config/cli-settings.layer.ts";
import { runtimeInfoLayer } from "../shared/runtime/runtime-info.layer.ts";
import { ttyLayer } from "../shared/runtime/tty.layer.ts";
import { cliConfigProviderLayer } from "../shared/config/cli-config-provider.layer.ts";
import { analyticsLayer } from "../telemetry/analytics.layer.ts";
import { formatCliError, normalizeCliError } from "../shared/output/normalize-error.ts";
Expand Down Expand Up @@ -1113,11 +1116,16 @@ export function captureCompleteTelemetryEffect(

const COMPLETE_TELEMETRY_TIMEOUT = "2 seconds";

// `analyticsLayer` needs `CliSettings`/`RuntimeInfo`/`Tty` plus a platform layer;
// `standaloneAnalyticsConfigLayer` packages that small set for a caller outside the full
// CLI runtime tree.
// Completion telemetry fires before `runCli` builds its layer tree, so the
// `CliSettings`/`RuntimeInfo`/`Tty` set that `analyticsLayer` needs is resolved here.
const completeAnalyticsLayer = analyticsLayer.pipe(
Layer.provide(standaloneAnalyticsConfigLayer),
Layer.provide(
Layer.mergeAll(
cliSettingsLayer.pipe(Layer.provide(cliProjectContextLayer), Layer.provide(runtimeInfoLayer)),
runtimeInfoLayer,
ttyLayer,
),
),
Layer.provide(FetchHttpClient.layer),
Layer.provide(cliConfigProviderLayer),
Layer.provide(BunServices.layer),
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/cli/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ import { telemetryCommand } from "../commands/telemetry/telemetry.command.ts";
import { unlinkCommand } from "../commands/unlink/unlink.command.ts";
import { vanitySubdomainsCommand } from "../commands/vanity-subdomains/vanity-subdomains.command.ts";
import { whoamiCommand } from "../commands/whoami/whoami.command.ts";
import { OutputFormatFlag } from "../shared/cli/global-flags.ts";
import { CLI_VERSION, cliBuildChannel } from "../shared/cli/version.ts";
import { outputLayerFor } from "../shared/output/output.layer.ts";
import { quietProgressTextOutputLayer } from "../output/quiet-progress-text-output.layer.ts";
Expand All @@ -71,6 +70,7 @@ import {
ExperimentalFlag,
NetworkIdFlag,
OutputFlag,
OutputFormatFlag,
ProfileFlag,
WorkdirFlag,
YesFlag,
Expand Down
14 changes: 0 additions & 14 deletions apps/cli/src/command-internal/db-bootstrap/messages.ts

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,15 @@ import {
} from "./db-setup.ts";
import type { ImagePrepullError } from "./image-prepull.ts";
import { waitForHealthyServices, type HealthCheckTimeoutError } from "./health-check.ts";
import {
START_STARTING_DATABASE_FROM_BACKUP_MESSAGE,
START_STARTING_DATABASE_MESSAGE,
} from "./messages.ts";
import {
buildPostgresStartContainerSpec,
type PostgresStartServiceInput,
} from "./postgres.service.ts";

const START_STARTING_DATABASE_MESSAGE = "Starting database...\n";
// Printed when an existing volume is reused; unrelated to `--from-backup`.
const START_STARTING_DATABASE_FROM_BACKUP_MESSAGE = "Starting database from backup...\n";

type Spawner = ChildProcessSpawner["Service"];

/**
Expand Down
7 changes: 0 additions & 7 deletions apps/cli/src/command-internal/db-connection.layer.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@ import { Duration, Effect } from "effect";
import { SUGGEST_ENV_VAR, SUGGEST_LOCAL_STACK } from "./connect-errors.ts";
import type { DbConnectError, DbExecError } from "./db-connection.errors.ts";
import { type DbSession, type PgConnInput, DbConnection } from "./db-connection.service.ts";
import {
acquirePgPool,
dbConnectionSqlPgLayer,
PgBatchQuery,
} from "./db-connection.sql-pg.layer.ts";
import { acquirePgPool, dbConnectionLayer, PgBatchQuery } from "./db-connection.sql-pg.layer.ts";

const SUGGESTION_CONTEXT = {
dashboardUrl: "https://supabase.com/dashboard",
Expand Down Expand Up @@ -55,7 +51,7 @@ const connectFailure = (
Effect.mapError(() => new Error("expected the connection to fail")),
Effect.orDie,
);
}).pipe(Effect.provide(dbConnectionSqlPgLayer));
}).pipe(Effect.provide(dbConnectionLayer));

/** A TCP port that is guaranteed closed: bind an ephemeral port, then release it. */
const acquireClosedPort = (): Promise<number> =>
Expand Down Expand Up @@ -403,7 +399,7 @@ const fakeQueryServer = (
});
});

describe("dbConnectionSqlPgLayer connect failures", () => {
describe("dbConnectionLayer connect failures", () => {
it.live(
"surfaces host, user, database, and the driver cause when a remote (--linked) connection is refused",
() =>
Expand Down Expand Up @@ -487,7 +483,7 @@ describe("dbConnectionSqlPgLayer connect failures", () => {
);
});

describe("dbConnectionSqlPgLayer exec failures", () => {
describe("dbConnectionLayer exec failures", () => {
it.live(
"maps a real wire ErrorResponse to pgconn's PgError rendering with detail and position",
() =>
Expand Down Expand Up @@ -540,7 +536,7 @@ describe("dbConnectionSqlPgLayer exec failures", () => {
Effect.mapError(() => new Error("expected the statement to fail")),
Effect.orDie,
);
}).pipe(Effect.provide(dbConnectionSqlPgLayer), Effect.ensuring(Effect.sync(server.close)));
}).pipe(Effect.provide(dbConnectionLayer), Effect.ensuring(Effect.sync(server.close)));
expect(error._tag).toBe("DbExecError");
expect(error.message).toBe('FEHLER: type "ltree" does not exist (SQLSTATE 42704)');
if (error._tag === "DbExecError") {
Expand All @@ -552,7 +548,7 @@ describe("dbConnectionSqlPgLayer exec failures", () => {
);
});

describe("dbConnectionSqlPgLayer extended batches", () => {
describe("dbConnectionLayer extended batches", () => {
/**
* Narrow a batch failure to its statement-execution error. `execBatch` also fails
* with `DbConnectError` when it cannot check a connection out of the pool,
Expand Down Expand Up @@ -584,7 +580,7 @@ describe("dbConnectionSqlPgLayer extended batches", () => {
return yield* use(session);
}).pipe(
Effect.scoped,
Effect.provide(dbConnectionSqlPgLayer),
Effect.provide(dbConnectionLayer),
Effect.ensuring(Effect.sync(server.close)),
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1054,4 +1054,5 @@ const connect = (
return session;
});

export const dbConnectionSqlPgLayer = Layer.succeed(DbConnection, { connect });
/** The active `DbConnection` layer, backed by `@effect/sql-pg`. */
export const dbConnectionLayer = Layer.succeed(DbConnection, { connect });
63 changes: 63 additions & 0 deletions apps/cli/src/command-internal/db-target-flags.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import {
changedLinkedLocalFlags,
resolveDbTargetFlags,
VALUE_CONSUMING_LONG_FLAGS,
VALUE_CONSUMING_SHORT_FLAGS,
Expand Down Expand Up @@ -258,3 +259,65 @@ describe("VALUE_CONSUMING_LONG_FLAGS / VALUE_CONSUMING_SHORT_FLAGS completeness
expect(missing).toEqual([]);
});
});

describe("changedLinkedLocalFlags", () => {
it("returns nothing when neither selector is present", () => {
expect(changedLinkedLocalFlags(["seed", "buckets"])).toEqual([]);
expect(changedLinkedLocalFlags(["storage", "ls", "ss:///"])).toEqual([]);
});

it("returns a single selector", () => {
expect(changedLinkedLocalFlags(["seed", "buckets", "--linked"])).toEqual(["linked"]);
expect(changedLinkedLocalFlags(["seed", "buckets", "--local"])).toEqual(["local"]);
});

it("returns both selectors in cobra's sorted order when both are set", () => {
expect(changedLinkedLocalFlags(["seed", "buckets", "--local", "--linked"])).toEqual([
"linked",
"local",
]);
});

it("handles = forms", () => {
expect(changedLinkedLocalFlags(["--local=true", "--linked=false"])).toEqual([
"linked",
"local",
]);
});

it("treats the --no-* negation form as changed", () => {
expect(changedLinkedLocalFlags(["seed", "buckets", "--no-linked"])).toEqual(["linked"]);
expect(changedLinkedLocalFlags(["storage", "ls", "--no-local"])).toEqual(["local"]);
expect(changedLinkedLocalFlags(["seed", "buckets", "--no-local", "--linked"])).toEqual([
"linked",
"local",
]);
});

it("does not treat a value-consuming flag's value as a selector", () => {
expect(changedLinkedLocalFlags(["seed", "buckets", "--workdir", "--linked"])).toEqual([]);
expect(changedLinkedLocalFlags(["-o", "--linked", "--local"])).toEqual(["local"]);
expect(
changedLinkedLocalFlags(["storage", "cp", "--content-type", "--local", "a", "b"]),
).toEqual([]);
expect(
changedLinkedLocalFlags(["storage", "cp", "--cache-control", "--linked", "a", "b"]),
).toEqual([]);
expect(changedLinkedLocalFlags(["storage", "cp", "--jobs", "--local", "a", "b"])).toEqual([]);
expect(changedLinkedLocalFlags(["storage", "cp", "-j", "--linked", "a", "b"])).toEqual([]);
});

it("still detects a selector after a value-consuming flag's value", () => {
expect(changedLinkedLocalFlags(["storage", "cp", "--jobs", "5", "--local", "a", "b"])).toEqual([
"local",
]);
});

it("stops scanning at the -- terminator", () => {
expect(changedLinkedLocalFlags(["seed", "buckets", "--", "--local", "--linked"])).toEqual([]);
});

it("detects selectors given after positional arguments", () => {
expect(changedLinkedLocalFlags(["storage", "rm", "ss:///b/x", "--local"])).toEqual(["local"]);
});
});
13 changes: 13 additions & 0 deletions apps/cli/src/command-internal/global-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ export const OutputFlag = GlobalFlag.setting("output")({
),
});

/**
* The TS-only `--output-format` global, accepted on any subcommand.
*
* A value-taking global flag must also be registered in `PERSISTENT_VALUE_FLAG_NAMES`
* (`shared/cli/cobra-flag-groups.ts`), or the pre-parse argv scanners won't consume its value.
*/
export const OutputFormatFlag = GlobalFlag.setting("output-format")({
flag: Flag.choice("output-format", ["text", "json", "stream-json"]).pipe(
Flag.withDescription("Output format: text (default), json, or stream-json (NDJSON)"),
Flag.optional,
),
});

export const ProfileFlag = GlobalFlag.setting("profile")({
flag: Flag.string("profile").pipe(
Flag.withDescription("use a specific profile for connecting to Supabase API"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Layer } from "effect";
import { httpClientLayer } from "../auth/http-debug.layer.ts";
import { commandSettingsLayer } from "../config/command-settings.layer.ts";
import { dbConfigLayer } from "./db-config.layer.ts";
import { dbConnectionLayer } from "./db-connection.layer.ts";
import { dbConnectionLayer } from "./db-connection.sql-pg.layer.ts";
import { debugLoggerLayer } from "./debug-logger.layer.ts";
import { dockerRunLayer } from "./docker-run.layer.ts";
import { edgeRuntimeScriptLayer } from "./edge-runtime-script.layer.ts";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const REGION_NAMES: Readonly<Record<string, string>> = {
"us-west-2": "West US (Oregon)",
};

export function formatRegion(region: string): string {
/** Renders a known region code as its display name; unknown codes pass through. */
export function formatRegion(region: string) {
return REGION_NAMES[region] ?? region;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";

import { formatRegion } from "./backups.format.ts";
import { formatRegion } from "./region.format.ts";

describe("formatRegion", () => {
it.each([
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/command-internal/stack-catalog-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import { parseConnectionString } from "./db-config.parse.ts";
import type { DbConnectError } from "./db-connection.errors.ts";
import { DbConnection } from "./db-connection.service.ts";
import { dbConnectionLayer } from "./db-connection.layer.ts";
import { dbConnectionLayer } from "./db-connection.sql-pg.layer.ts";
import {
applyDatabaseOverlay,
type ApplyDatabaseOverlayInput,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { mockOutput } from "../../tests/helpers/mocks.ts";
import { mockCommandSettings } from "../../tests/helpers/command-mocks.ts";
import { runtimeInfoLayer } from "../shared/runtime/runtime-info.layer.ts";
import { DbConnection } from "./db-connection.service.ts";
import { dbConnectionLayer } from "./db-connection.layer.ts";
import { dbConnectionLayer } from "./db-connection.sql-pg.layer.ts";
import { StackApi, stackApiLayer } from "./stack-api.ts";
import { StackCatalogSetup, stackCatalogSetupLayer } from "./stack-catalog-setup.ts";
import { parseConnectionString } from "./db-config.parse.ts";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Effect, FileSystem, Layer, Option, Path, Stream } from "effect";
import { FetchHttpClient } from "effect/unstable/http";

import { CommandPlatformApiFactory } from "../auth/command-platform-api-factory.service.ts";
import { dbConnectionLayer } from "./db-connection.layer.ts";
import { dbConnectionLayer } from "./db-connection.sql-pg.layer.ts";
import { stackCatalogSetupLayer } from "./stack-catalog-setup.ts";
import {
classifyStorageCapability,
Expand Down
Loading
Loading