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
6 changes: 3 additions & 3 deletions apps/cli/src/cli/boolean-flag-defaults.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@ import { unwrapParam } from "../command-internal/param-introspection.ts";
import { rootCommandForFeatures } from "./root.ts";

/**
* `Flag.boolean(name)` alone builds a required param, so omitting it fails the whole command
* `Flag.Boolean(name)` alone builds a required param, so omitting it fails the whole command
* with a missing-flag error before the handler runs — every boolean flag must pair with
* `Flag.withDefault(false)` or `Flag.optional`. No other test catches this: handler integration
* tests build their flags record directly, bypassing the parser, and the type checker can't see
* the required-ness since it still infers as `boolean`.
*/

/**
* Uses `Primitive.getTypeName(Primitive.boolean)` rather than the literal `"boolean"`, so a
* Uses `Primitive.getTypeName(Primitive.Boolean)` rather than the literal `"boolean"`, so a
* rename upstream breaks loudly instead of silently matching nothing and passing every command.
* Avoids `primitiveType._tag` to keep this guard off effect's runtime representation.
*/
const BOOLEAN_TYPE_NAME = Primitive.getTypeName(Primitive.boolean);
const BOOLEAN_TYPE_NAME = Primitive.getTypeName(Primitive.Boolean);

function booleanFlagsRequiringAValue(command: Command.Command.Any): ReadonlyArray<string> {
const internals = commandInternals(command);
Expand Down
12 changes: 6 additions & 6 deletions apps/cli/src/cli/complete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export interface FlagDescriptor {
readonly description: string | undefined;
readonly isVariadic: boolean;
readonly isBoolean: boolean;
/** `Param.Single`'s underlying `Primitive<A>._tag` (`"Boolean"`, `"Choice"`, `"Integer"`, ...). */
/** `Param.Single`'s underlying `Primitive<A>._tag` (`"Boolean"`, `"Choice"`, `"Int"`, ...). */
readonly primitiveTag: string;
/** The valid value set for a `primitiveTag === "Choice"` flag; `undefined` for every other tag. */
readonly choiceKeys: ReadonlyArray<string> | undefined;
Expand Down Expand Up @@ -456,8 +456,8 @@ function hasUnconsumedFlagTerminator(

/**
* Flags whose real validation rejects a leading `-`/`+`, unlike this tree's plain signed
* `Flag.integer`/`Flag.string` declarations — checked before `primitiveTag` dispatch since
* `storage cp --jobs` is declared as `Flag.string`. Key = `<matched command path>:<flag name>`.
* `Flag.Int`/`Flag.String` declarations — checked before `primitiveTag` dispatch since
* `storage cp --jobs` is declared as `Flag.String`. Key = `<matched command path>:<flag name>`.
*/
const COMPLETION_UINT_FLAGS: ReadonlySet<string> = new Set([
"functions deploy:jobs",
Expand All @@ -468,7 +468,7 @@ const COMPLETION_UINT_FLAGS: ReadonlySet<string> = new Set([

/**
* Flags validated against Go duration syntax (see `isValidGoDuration`) rather than this
* tree's plain `Flag.string` declarations.
* tree's plain `Flag.String` declarations.
*/
const COMPLETION_DURATION_FLAGS: ReadonlySet<string> = new Set([
"gen types:query-timeout",
Expand Down Expand Up @@ -685,9 +685,9 @@ function isValidFlagValue(
return outputFlagChoiceKeys(matchedPath).includes(value);
}
return flag.choiceKeys !== undefined && flag.choiceKeys.includes(value);
case "Integer":
case "Int":
return isValidBase0Int64(value);
case "Float":
case "Finite":
return value.trim().length > 0 && !Number.isNaN(Number(value));
default:
return true;
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/src/cli/complete.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ describe("respondToComplete", () => {
{ path: ["db", "reset"], flag: "last" },
])("rejects a negative value for $path --$flag", ({ path, flag }) => {
// These flags are validated as unsigned integers, which reject a leading sign,
// unlike this tree's plain signed Flag.integer regex.
// unlike this tree's plain signed Flag.Int regex.
const result = respondToComplete(rootCommand, ["__complete", ...path, `--${flag}`, "-1", ""]);
expect(result).toEqual({ candidates: [], directive: CompletionDirective.Default });
});
Expand Down Expand Up @@ -593,7 +593,7 @@ describe("respondToComplete", () => {

it("rejects a value one past int64 max for a plain (non-uint) integer flag", () => {
// Validated against the signed int64 range, which is narrower than the uint64
// bound `Flag.integer` alone would suggest.
// bound `Flag.Int` alone would suggest.
const overflow = respondToComplete(rootCommand, [
"__complete",
"backups",
Expand Down
5 changes: 3 additions & 2 deletions apps/cli/src/command-internal/db-bootstrap/shadow-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -758,8 +758,9 @@ const SHADOW_CACHE_KEY_PATTERN = /^[0-9a-f]{16}$/u;
*/
const describeShadowArchiveProblem = (problem: PgDataArchiveProblem): string =>
Match.valueTags(problem, {
"missing-entries": (missing) => `snapshot has no ${missing.entries.join(" or ")} entry`,
"wrong-key": (wrongKey) => {
"missing-entries": (missing: Extract<PgDataArchiveProblem, { _tag: "missing-entries" }>) =>
`snapshot has no ${missing.entries.join(" or ")} entry`,
"wrong-key": (wrongKey: Extract<PgDataArchiveProblem, { _tag: "wrong-key" }>) => {
const found =
wrongKey.found !== undefined && SHADOW_CACHE_KEY_PATTERN.test(wrongKey.found)
? `key ${wrongKey.found}`
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/src/command-internal/db-target-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ export const VALUE_CONSUMING_LONG_FLAGS = new Set([
"password",
// inspect report flag
"output-dir",
// storage cp command flags (Flag.string / Flag.integer)
// storage cp command flags (Flag.String / Flag.Int)
"cache-control",
"content-type",
"jobs",
// global flags (Flag.string / Flag.choice)
// global flags (Flag.String / Flag.Literals)
"output",
"output-format",
"profile",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ describe("VALUE_CONSUMING_LONG_FLAGS / VALUE_CONSUMING_SHORT_FLAGS completeness
const current = calls[i]!;
if (!VALUE_FLAG_KINDS.includes(current.kind)) continue;

// Name declared as a literal string (e.g. `Flag.string("schema")`); a name passed as an
// Name declared as a literal string (e.g. `Flag.String("schema")`); a name passed as an
// identifier doesn't match and is silently skipped — see INDIRECT_NAME_FILES above.
const remainder = source.slice(current.index);
const nameMatch = remainder.match(/^Flag\.\w+\(\s*"([a-zA-Z0-9-]+)"/);
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/src/command-internal/glob.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BunFileSystem, BunPath } from "@effect/platform-bun";
import { describe, expect, it } from "@effect/vitest";
import { Effect, Exit, FileSystem, Layer, Option, Path, PlatformError } from "effect";
import { ByteSize, Effect, Exit, FileSystem, Layer, Option, Path, PlatformError } from "effect";

import { compareUtf8Bytes, globPattern, resolveUnderWorkdir, walkSqlFiles } from "./glob.ts";

Expand Down Expand Up @@ -88,7 +88,7 @@ function fakeFileInfo(type: FileSystem.File.Type): FileSystem.File.Info {
uid: Option.none(),
gid: Option.none(),
rdev: Option.none(),
size: 0n as FileSystem.Size,
size: ByteSize.bytes(0),
blksize: Option.none(),
blocks: Option.none(),
};
Expand Down
40 changes: 20 additions & 20 deletions apps/cli/src/command-internal/global-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,74 +27,74 @@ export const GLOBAL_OUTPUT_FORMATS = [
"csv",
] as const;

export const OutputFlag = GlobalFlag.setting("output")({
flag: Flag.choice("output", GLOBAL_OUTPUT_FORMATS).pipe(
export const OutputFlag = GlobalFlag.Setting("output")({
flag: Flag.Literals("output", GLOBAL_OUTPUT_FORMATS).pipe(
Flag.withAlias("o"),
Flag.withDescription("output format of status variables"),
Flag.optional,
),
});

export const ProfileFlag = GlobalFlag.setting("profile")({
flag: Flag.string("profile").pipe(
export const ProfileFlag = GlobalFlag.Setting("profile")({
flag: Flag.String("profile").pipe(
Flag.withDescription("use a specific profile for connecting to Supabase API"),
Flag.withDefault("supabase"),
),
});

export const DebugFlag = GlobalFlag.setting("debug")({
flag: Flag.boolean("debug").pipe(
export const DebugFlag = GlobalFlag.Setting("debug")({
flag: Flag.Boolean("debug").pipe(
Flag.withDescription("output debug logs to stderr"),
Flag.withDefault(false),
),
});

export const WorkdirFlag = GlobalFlag.setting("workdir")({
flag: Flag.string("workdir").pipe(
export const WorkdirFlag = GlobalFlag.Setting("workdir")({
flag: Flag.String("workdir").pipe(
Flag.withDescription(
"path to the directory containing your supabase/ folder; used exactly as given, with no ancestor directory search (defaults to searching upward from the current directory)",
),
Flag.optional,
),
});

export const ExperimentalFlag = GlobalFlag.setting("experimental")({
flag: Flag.boolean("experimental").pipe(
export const ExperimentalFlag = GlobalFlag.Setting("experimental")({
flag: Flag.Boolean("experimental").pipe(
Flag.withDescription("enable experimental features"),
Flag.withDefault(false),
),
});

export const NetworkIdFlag = GlobalFlag.setting("network-id")({
flag: Flag.string("network-id").pipe(
export const NetworkIdFlag = GlobalFlag.Setting("network-id")({
flag: Flag.String("network-id").pipe(
Flag.withDescription("use the specified docker network instead of a generated one"),
Flag.optional,
),
});

export const YesFlag = GlobalFlag.setting("yes")({
flag: Flag.boolean("yes").pipe(
export const YesFlag = GlobalFlag.Setting("yes")({
flag: Flag.Boolean("yes").pipe(
Flag.withDescription("answer yes to all prompts"),
Flag.withDefault(false),
),
});

export const DnsResolverFlag = GlobalFlag.setting("dns-resolver")({
flag: Flag.choice("dns-resolver", ["native", "https"] as const).pipe(
export const DnsResolverFlag = GlobalFlag.Setting("dns-resolver")({
flag: Flag.Literals("dns-resolver", ["native", "https"] as const).pipe(
Flag.withDescription("lookup domain names using the specified resolver"),
Flag.withDefault("native" as const),
),
});

export const CreateTicketFlag = GlobalFlag.setting("create-ticket")({
flag: Flag.boolean("create-ticket").pipe(
export const CreateTicketFlag = GlobalFlag.Setting("create-ticket")({
flag: Flag.Boolean("create-ticket").pipe(
Flag.withDescription("create a support ticket for any CLI error"),
Flag.withDefault(false),
),
});

export const AgentFlag = GlobalFlag.setting("agent")({
flag: Flag.choice("agent", ["auto", "yes", "no"] as const).pipe(
export const AgentFlag = GlobalFlag.Setting("agent")({
flag: Flag.Literals("agent", ["auto", "yes", "no"] as const).pipe(
Flag.withDescription("Override agent detection: yes, no, or auto (default auto)"),
Flag.withDefault("auto" as const),
),
Expand Down
24 changes: 12 additions & 12 deletions apps/cli/src/command-internal/param-introspection.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,82 +5,82 @@ import { unwrapParam, unwrapToSingleParam } from "./param-introspection.ts";

describe("unwrapParam", () => {
it("unwraps a plain, required flag with isOptional/isVariadic both false and variadicMin 0", () => {
const result = unwrapParam(Flag.string("custom-hostname"));
const result = unwrapParam(Flag.String("custom-hostname"));
expect(result?.single.name).toBe("custom-hostname");
expect(result?.isOptional).toBe(false);
expect(result?.isVariadic).toBe(false);
expect(result?.variadicMin).toBe(0);
});

it("marks a Flag.optional-wrapped flag as isOptional", () => {
const result = unwrapParam(Flag.string("desired-subdomain").pipe(Flag.optional));
const result = unwrapParam(Flag.String("desired-subdomain").pipe(Flag.optional));
expect(result?.single.name).toBe("desired-subdomain");
expect(result?.isOptional).toBe(true);
});

it("marks a Flag.withDefault-wrapped flag as isOptional (composes as Map(Optional(Single)))", () => {
const result = unwrapParam(Flag.string("profile").pipe(Flag.withDefault("supabase")));
const result = unwrapParam(Flag.String("profile").pipe(Flag.withDefault("supabase")));
expect(result?.single.name).toBe("profile");
expect(result?.isOptional).toBe(true);
});

it("does not mark a plain boolean flag as isOptional (booleans default to false unwrapped)", () => {
const result = unwrapParam(Flag.boolean("debug"));
const result = unwrapParam(Flag.Boolean("debug"));
expect(result?.single.name).toBe("debug");
expect(result?.isOptional).toBe(false);
expect(result?.single.primitiveType._tag).toBe("Boolean");
});

it("marks a zero-minimum variadic flag (Flag.atLeast(0)) as variadic but NOT optional, with variadicMin 0", () => {
const result = unwrapParam(Flag.string("domains").pipe(Flag.atLeast(0)));
const result = unwrapParam(Flag.String("domains").pipe(Flag.atLeast(0)));
expect(result?.single.name).toBe("domains");
expect(result?.isOptional).toBe(false);
expect(result?.isVariadic).toBe(true);
expect(result?.variadicMin).toBe(0);
});

it("captures a positive variadic minimum (Flag.atLeast(2))", () => {
const result = unwrapParam(Flag.string("source").pipe(Flag.atLeast(2)));
const result = unwrapParam(Flag.String("source").pipe(Flag.atLeast(2)));
expect(result?.isVariadic).toBe(true);
expect(result?.variadicMin).toBe(2);
});

it("captures the minimum from Flag.between", () => {
const result = unwrapParam(Flag.string("host").pipe(Flag.between(1, 3)));
const result = unwrapParam(Flag.String("host").pipe(Flag.between(1, 3)));
expect(result?.isVariadic).toBe(true);
expect(result?.variadicMin).toBe(1);
});

it("reports variadicMin 0 for an unbounded Flag.atMost (no minimum set)", () => {
const result = unwrapParam(Flag.string("warning").pipe(Flag.atMost(3)));
const result = unwrapParam(Flag.String("warning").pipe(Flag.atMost(3)));
expect(result?.isVariadic).toBe(true);
expect(result?.variadicMin).toBe(0);
});

it("walks through a chained Map after Optional (Flag.withDefault on a choice flag)", () => {
const result = unwrapParam(
Flag.choice("dns-resolver", ["native", "https"] as const).pipe(Flag.withDefault("native")),
Flag.Literals("dns-resolver", ["native", "https"] as const).pipe(Flag.withDefault("native")),
);
expect(result?.single.name).toBe("dns-resolver");
expect(result?.isOptional).toBe(true);
expect(result?.single.primitiveType._tag).toBe("Choice");
});

it("preserves aliases and hidden metadata on the underlying Single", () => {
const result = unwrapParam(Flag.string("type").pipe(Flag.withAlias("t"), Flag.withHidden));
const result = unwrapParam(Flag.String("type").pipe(Flag.withAlias("t"), Flag.withHidden));
expect(result?.single.aliases).toEqual(["t"]);
expect(result?.single.hidden).toBe(true);
});
});

describe("unwrapToSingleParam", () => {
it("returns just the underlying Single, discarding optional/variadic metadata", () => {
const single = unwrapToSingleParam(Flag.string("role").pipe(Flag.optional));
const single = unwrapToSingleParam(Flag.String("role").pipe(Flag.optional));
expect(single?.name).toBe("role");
});

it("agrees with unwrapParam's own .single for the same input", () => {
const param = Flag.string("status").pipe(Flag.atLeast(0));
const param = Flag.String("status").pipe(Flag.atLeast(0));
expect(unwrapToSingleParam(param)).toBe(unwrapParam(param)?.single);
});
});
2 changes: 1 addition & 1 deletion apps/cli/src/command-internal/string-slice-flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ export function stringSliceFlag(
) {
const alias = options?.alias;
const pflagName = alias === undefined ? `--${name}` : `-${alias}, --${name}`;
const base = Flag.string(name).pipe(Flag.withDescription(description), Flag.atLeast(0));
const base = Flag.String(name).pipe(Flag.withDescription(description), Flag.atLeast(0));
return (alias === undefined ? base : base.pipe(Flag.withAlias(alias))).pipe(
Flag.mapTryCatch(
(rawValues) => parseStringSliceFlag(rawValues),
Expand Down
10 changes: 5 additions & 5 deletions apps/cli/src/command-internal/test-db.command-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,26 +38,26 @@ const onRunFailure = (error: TestDbRunError | TestDbNoTestsError) =>
* families — see "Hoist Before You Duplicate" in `apps/cli/CLAUDE.md`.
*/
export const testDbConfig = {
paths: Argument.string("path").pipe(
paths: Argument.String("path").pipe(
Argument.withDescription("Paths to test files or directories."),
Argument.variadic(),
),
dbUrl: Flag.string("db-url").pipe(
dbUrl: Flag.String("db-url").pipe(
Flag.withDescription(
"Tests the database specified by the connection string (must be percent-encoded).",
),
Flag.optional,
),
linked: Flag.boolean("linked").pipe(
linked: Flag.Boolean("linked").pipe(
Flag.withDescription("Runs pgTAP tests on the linked project."),
Flag.withDefault(false),
),
local: Flag.boolean("local").pipe(
local: Flag.Boolean("local").pipe(
Flag.withDescription("Runs pgTAP tests on the local database."),
Flag.withDefault(false),
),
// TS-only override of the linked project ref — see push.command.ts (db push).
projectRef: Flag.string("project-ref").pipe(
projectRef: Flag.String("project-ref").pipe(
Flag.withDescription("Project ref of the Supabase project."),
Flag.optional,
),
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/commands/backups/list/list.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { withCommandTelemetry } from "../../../telemetry/command-telemetry.ts";
import { backupsList } from "./list.handler.ts";

const config = {
projectRef: Flag.string("project-ref").pipe(
projectRef: Flag.String("project-ref").pipe(
Flag.withDescription("Project ref of the Supabase project."),
Flag.optional,
),
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/src/commands/backups/restore/restore.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import { withCommandTelemetry } from "../../../telemetry/command-telemetry.ts";
import { backupsRestore } from "./restore.handler.ts";

const config = {
projectRef: Flag.string("project-ref").pipe(
projectRef: Flag.String("project-ref").pipe(
Flag.withDescription("Project ref of the Supabase project."),
Flag.optional,
),
timestamp: Flag.integer("timestamp").pipe(
timestamp: Flag.Int("timestamp").pipe(
Flag.withAlias("t"),
Flag.withDescription("The recovery time target in seconds since epoch."),
Flag.optional,
Expand Down
Loading