Skip to content
Open
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
40 changes: 39 additions & 1 deletion .oxlintrc.effect.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@
"!apps/cli/tests/helpers/postgres-config-live.ts",
"!apps/cli/tests/helpers/secrets-live.ts",
"!apps/cli/tests/helpers/storage-live.ts",
"!apps/cli/src/command-internal/agent-mode.*",
"!apps/cli/src/command-internal/api-*",
"!apps/cli/src/command-internal/bitbucket-pipeline.ts",
"!apps/cli/src/command-internal/branch-ref.*",
"!apps/cli/src/command-internal/branch-target.*",
"!apps/cli/src/command-internal/bundled-postgres-client.*",
"!apps/cli/src/command-internal/colors.*",
"!apps/cli/src/command-internal/connect-errors.*",
"!apps/cli/src/command-internal/container-cli.*",
"!apps/cli/src/command-internal/db-*.ts",
Expand All @@ -36,10 +39,22 @@
"!apps/cli/src/command-internal/drop-objects.ts",
"!apps/cli/src/command-internal/edge-runtime-*.ts",
"!apps/cli/src/command-internal/ensure-login.*",
"!apps/cli/src/command-internal/error-message.ts",
"!apps/cli/src/command-internal/experimental-feature.ts",
"!apps/cli/src/command-internal/experimental-gate.*",
"!apps/cli/src/command-internal/fail-on.*",
"!apps/cli/src/command-internal/get-api-keys.*",
"!apps/cli/src/command-internal/get-tenant-api-keys.*",
"!apps/cli/src/command-internal/git-status.*",
"!apps/cli/src/command-internal/glob.*",
"!apps/cli/src/command-internal/global-flags.*",
"!apps/cli/src/command-internal/go-duration.*",
"!apps/cli/src/command-internal/go-float.*",
"!apps/cli/src/command-internal/go-json.*",
"!apps/cli/src/command-internal/go-output-flag.*",
"!apps/cli/src/command-internal/go-output.encoders.*",
"!apps/cli/src/command-internal/go-quote.ts",
"!apps/cli/src/command-internal/go-struct-output.*",
"!apps/cli/src/command-internal/hostname.*",
"!apps/cli/src/command-internal/http-*",
"!apps/cli/src/command-internal/identity-stitch.*",
Expand All @@ -48,11 +63,16 @@
"!apps/cli/src/command-internal/linked-state.*",
"!apps/cli/src/command-internal/local-project-context.*",
"!apps/cli/src/command-internal/login-*",
"!apps/cli/src/command-internal/make-dir.*",
"!apps/cli/src/command-internal/management-api-runtime.*",
"!apps/cli/src/command-internal/migrate-and-seed.*",
"!apps/cli/src/command-internal/migration*",
"!apps/cli/src/command-internal/param-introspection.*",
"!apps/cli/src/command-internal/parent-project-ref.*",
"!apps/cli/src/command-internal/parse-uint.*",
"!apps/cli/src/command-internal/password-requirements.ts",
"!apps/cli/src/command-internal/path-match.*",
"!apps/cli/src/command-internal/pflag-reconcile.*",
"!apps/cli/src/command-internal/pg-dump.*",
"!apps/cli/src/command-internal/pg-net-guidance.*",
"!apps/cli/src/command-internal/pgdelta*",
Expand All @@ -66,11 +86,19 @@
"!apps/cli/src/command-internal/project-create-core.*",
"!apps/cli/src/command-internal/project-environment.*",
"!apps/cli/src/command-internal/project-target.*",
"!apps/cli/src/command-internal/prompt-yes-no.*",
"!apps/cli/src/command-internal/raw-http.*",
"!apps/cli/src/command-internal/ref-patterns.*",
"!apps/cli/src/command-internal/region.format.*",
"!apps/cli/src/command-internal/resolve-token.*",
"!apps/cli/src/command-internal/rune-width.*",
"!apps/cli/src/command-internal/schema-flags.*",
"!apps/cli/src/command-internal/seed*",
"!apps/cli/src/command-internal/service-catalog.*",
"!apps/cli/src/command-internal/service-version-overrides.*",
"!apps/cli/src/command-internal/shell-quote.ts",
"!apps/cli/src/command-internal/size-units.ts",
"!apps/cli/src/command-internal/sql-files-glob.*",
"!apps/cli/src/command-internal/sql-split.*",
"!apps/cli/src/command-internal/stack-api.ts",
"!apps/cli/src/command-internal/stack-backend.ts",
Expand All @@ -79,10 +107,20 @@
"!apps/cli/src/command-internal/stack-local-database.ts",
"!apps/cli/src/command-internal/stack-shadow.ts",
"!apps/cli/src/command-internal/stack-storage.ts",
"!apps/cli/src/command-internal/status-errors.ts",
"!apps/cli/src/command-internal/status-pretty.*",
"!apps/cli/src/command-internal/status-values.*",
"!apps/cli/src/command-internal/storage-*.ts",
"!apps/cli/src/command-internal/string-slice-flag.*",
"!apps/cli/src/command-internal/temp-paths.*",
"!apps/cli/src/command-internal/tenant-*",
"!apps/cli/src/command-internal/timestamp.format.*",
"!apps/cli/src/command-internal/upgrade-suggest.*",
"!apps/cli/src/command-internal/vault-decrypt.*",
"!apps/cli/src/command-internal/vault.*",
"!apps/cli/src/command-internal/viper-env.*"
"!apps/cli/src/command-internal/viper-env.*",
"!apps/cli/src/command-internal/workdir-project.*",
"!apps/cli/src/command-internal/workdir-search.ts",
"!apps/cli/src/command-internal/workdir-validation.ts"
]
}
21 changes: 12 additions & 9 deletions apps/cli/src/command-internal/git-status.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { basename, dirname } from "node:path";
import { Effect, Option } from "effect";
import { Effect, Option, Path } from "effect";
import * as ChildProcess from "effect/process/ChildProcess";
import { ChildProcessSpawner } from "effect/process/ChildProcessSpawner";

Expand All @@ -17,11 +16,10 @@ import { collectText } from "./container-cli.ts";
* Returns `Option.none()` whenever the answer can't be determined (not a git working tree, `git`
* missing, a non-zero exit, or any other failure) rather than treating that as "clean".
*/
export function pathHasUncommittedChanges(
path: string,
): Effect.Effect<Option.Option<boolean>, never, ChildProcessSpawner> {
const args = ["status", "--porcelain", "--", basename(path)];
return withProcessSpan(
export const pathHasUncommittedChanges = Effect.fnUntraced(function* (path: string) {
const pathService = yield* Path.Path;
const args = ["status", "--porcelain", "--", pathService.basename(path)];
return yield* withProcessSpan(
"Git.status",
{ executable: "git", argCount: args.length },
(traceEnv) =>
Expand All @@ -33,7 +31,12 @@ export function pathHasUncommittedChanges(
"git",
args,
withChildTraceEnv(
{ cwd: dirname(path), stdin: "ignore", stdout: "pipe", stderr: "ignore" },
{
cwd: pathService.dirname(path),
stdin: "ignore",
stdout: "pipe",
stderr: "ignore",
},
traceEnv,
),
),
Expand All @@ -52,4 +55,4 @@ export function pathHasUncommittedChanges(
),
Effect.orElseSucceed(() => Option.none<boolean>()),
);
}
});
22 changes: 13 additions & 9 deletions apps/cli/src/command-internal/git-status.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "@effect/vitest";
import { Deferred, Effect, Option, PlatformError, Sink, Stream } from "effect";
import { Deferred, Effect, Option, Path, PlatformError, Sink, Stream } from "effect";
import { ChildProcessSpawner } from "effect/process";

import { pathHasUncommittedChanges } from "./git-status.ts";
Expand All @@ -26,14 +26,12 @@ function mockSpawner(
spawned.push({ command: cmd, args, cwd });

if (opts.spawnFails === true) {
return yield* Effect.fail(
PlatformError.systemError({
_tag: "NotFound",
module: "ChildProcess",
method: "spawn",
description: `${cmd} not found`,
}),
);
return yield* PlatformError.systemError({
_tag: "NotFound",
module: "ChildProcess",
method: "spawn",
description: `${cmd} not found`,
});
}

const exitDeferred = yield* Deferred.make<ChildProcessSpawner.ExitCode>();
Expand Down Expand Up @@ -71,6 +69,7 @@ describe("pathHasUncommittedChanges", () => {
const mock = mockSpawner({ stdout: " M config.toml\n" });
return pathHasUncommittedChanges("/repo/supabase/config.toml").pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, mock.spawner),
Effect.provide(Path.layer),
Effect.map((result) => {
expect(Option.isSome(result)).toBe(true);
if (Option.isSome(result)) expect(result.value).toBe(true);
Expand All @@ -82,6 +81,7 @@ describe("pathHasUncommittedChanges", () => {
const mock = mockSpawner({ stdout: "" });
return pathHasUncommittedChanges("/repo/supabase/config.toml").pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, mock.spawner),
Effect.provide(Path.layer),
Effect.map((result) => {
expect(Option.isSome(result)).toBe(true);
if (Option.isSome(result)) expect(result.value).toBe(false);
Expand All @@ -93,6 +93,7 @@ describe("pathHasUncommittedChanges", () => {
const mock = mockSpawner({ exitCode: 128, stdout: "" });
return pathHasUncommittedChanges("/repo/supabase/config.toml").pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, mock.spawner),
Effect.provide(Path.layer),
Effect.map((result) => {
expect(Option.isNone(result)).toBe(true);
}),
Expand All @@ -103,6 +104,7 @@ describe("pathHasUncommittedChanges", () => {
const mock = mockSpawner({ spawnFails: true });
return pathHasUncommittedChanges("/repo/supabase/config.toml").pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, mock.spawner),
Effect.provide(Path.layer),
Effect.map((result) => {
expect(Option.isNone(result)).toBe(true);
}),
Expand All @@ -115,6 +117,7 @@ describe("pathHasUncommittedChanges", () => {
const mock = mockSpawner({ stdout: "" });
return pathHasUncommittedChanges("/repo/supabase/config.toml").pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, mock.spawner),
Effect.provide(Path.layer),
Effect.map(() => {
expect(mock.spawned).toEqual([
{
Expand All @@ -134,6 +137,7 @@ describe("pathHasUncommittedChanges", () => {
const mock = mockSpawner({ stdout: " M migrations/20260101000000_init.sql\n" });
return pathHasUncommittedChanges("/repo/supabase/migrations").pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, mock.spawner),
Effect.provide(Path.layer),
Effect.map((result) => {
expect(Option.isSome(result)).toBe(true);
if (Option.isSome(result)) expect(result.value).toBe(true);
Expand Down
8 changes: 5 additions & 3 deletions apps/cli/src/command-internal/go-struct-output.encoders.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Data } from "effect";

import {
actionability,
type CliErrorActionabilityDeclaration,
Expand Down Expand Up @@ -996,11 +998,11 @@ function yamlBlockLiteral(s: string, indent: number): string {
* `nullable.Nullable` field (`map[bool]T` has a non-string key type — observed
* on `snippets list -o toml`) or a `nil` element inside an inline array.
*/
export class GoTomlEncodeError extends Error {
export class GoTomlEncodeError extends Data.Error<{ readonly message: string }> {
static readonly [ErrorActionabilityFingerprintId] = "GoTomlEncodeError";
override readonly name = "GoTomlEncodeError";
constructor(message = "toml: cannot encode a map with non-string key type") {
super(message);
this.name = "GoTomlEncodeError";
super({ message });
}

get [ErrorActionabilityId](): CliErrorActionabilityDeclaration {
Expand Down
11 changes: 4 additions & 7 deletions apps/cli/src/command-internal/make-dir.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "@effect/vitest";
import { Effect, Exit, FileSystem, Layer, PlatformError } from "effect";
import { Effect, Exit, FileSystem, Layer, PlatformError, Predicate } from "effect";

import { makeDir } from "./make-dir.ts";

Expand Down Expand Up @@ -78,13 +78,10 @@ describe("makeDir", () => {
it.effect("propagates every other filesystem error", () => {
const fs = failingFs("PermissionDenied");
return run(DIR).pipe(
Effect.exit,
Effect.tap((exit) =>
Effect.flip,
Effect.tap((error) =>
Effect.sync(() => {
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
expect(JSON.stringify(exit.cause)).toContain("PermissionDenied");
}
expect(Predicate.isTagged(error.reason, "PermissionDenied")).toBe(true);
}),
),
Effect.provide(fs.layer),
Expand Down
20 changes: 10 additions & 10 deletions apps/cli/src/command-internal/pflag-reconcile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ import { Data, Effect, FileSystem, Option, Path, Result } from "effect";
import { CliArgs } from "../shared/cli/cli-args.service.ts";
import { lastExplicitLongFlagValue, type PflagArgvScan } from "../shared/cli/cobra-flag-groups.ts";
import { ProfileFlag, WorkdirFlag } from "./global-flags.ts";
import { RuntimeInfo } from "../shared/runtime/runtime-info.service.ts";
import {
actionability,
type CliErrorActionabilityDeclaration,
ErrorActionabilityId,
} from "../shared/telemetry/error-actionability.ts";
import { profileFilePath } from "../config/profile-file.ts";
import { CommandSettings } from "../config/command-settings.service.ts";
import { loadProfile, type LoadedProfile } from "./profile-load.ts";
import { parseStringSliceFlag } from "./string-slice-flag.ts";
import { validateWorkdirIsDirectory } from "./workdir-validation.ts";
Expand Down Expand Up @@ -136,7 +135,8 @@ export const validatePflagWorkdir = Effect.fnUntraced(function* (
// `serviceOption`: absent outside the real CLI tree (handler-level tests
// provide argv via `Stdio.layerTest`, not the global flag settings).
const parsedWorkdir = Option.flatten(yield* Effect.serviceOption(WorkdirFlag));
const workdir = pflagWorkdirValue(scan, parsedWorkdir, process.env["SUPABASE_WORKDIR"]);
const envWorkdir = Option.getOrUndefined((yield* CommandSettings).workdirEnvValue);
const workdir = pflagWorkdirValue(scan, parsedWorkdir, envWorkdir);
if (Option.isNone(workdir)) {
return;
}
Expand Down Expand Up @@ -205,16 +205,17 @@ export function pflagProfileValue(
* persisted-file token) or when the resolved token is empty. On every normal invocation the
* layer's resolution stands unchanged.
*
* `serviceOption` throughout: outside the real CLI tree (handler-level tests provide argv via
* `Stdio.layerTest`) the flag settings and `RuntimeInfo` may be absent, and the reconcile then
* only acts on what the scan itself shows.
* `serviceOption` for all but `CommandSettings`: outside the real CLI tree (handler-level
* tests provide argv via `Stdio.layerTest`) the flag settings may be absent, and the reconcile
* then only acts on what the scan itself shows.
*/
export const resolvePflagProfile = Effect.fnUntraced(function* (
scan: Pick<PflagArgvScan, "occurrences" | "consumedFlagNames" | "prePathOccurrences">,
) {
const parsedRaw = yield* Effect.serviceOption(ProfileFlag);
const parsedProfile = Option.filter(parsedRaw, (value) => value !== "supabase");
const env = process.env["SUPABASE_PROFILE"];
const settings = yield* CommandSettings;
const env = Option.getOrUndefined(settings.profileEnvValue);
const envProfile = env !== undefined && env.length > 0 ? env : undefined;

// The explicit token pflag-equivalent semantics resolve, vs. the one the config layer's own
Expand Down Expand Up @@ -245,16 +246,15 @@ export const resolvePflagProfile = Effect.fnUntraced(function* (

const fs = yield* Effect.serviceOption(FileSystem.FileSystem);
const path = yield* Effect.serviceOption(Path.Path);
const runtimeInfo = yield* Effect.serviceOption(RuntimeInfo);
if (Option.isNone(fs) || Option.isNone(path) || Option.isNone(runtimeInfo)) {
if (Option.isNone(fs) || Option.isNone(path)) {
return Option.none<LoadedProfile>();
}

// Lowest precedence: the persisted `~/.supabase/profile` file. This reads the raw bytes,
// while the config layer trims and maps empty to the default — a divergence the token
// comparison below surfaces (e.g. a trailing newline fails to load as a profile).
const fileRaw = yield* fs.value
.readFileString(profileFilePath(path.value, runtimeInfo.value.homeDir))
.readFileString(path.value.join(settings.supabaseHome, "profile"))
.pipe(Effect.option);

const goToken = Option.isSome(goExplicit)
Expand Down
Loading
Loading