diff --git a/.oxlintrc.effect.json b/.oxlintrc.effect.json index a3d69838c8..8ef77ee1e5 100644 --- a/.oxlintrc.effect.json +++ b/.oxlintrc.effect.json @@ -120,6 +120,7 @@ "!apps/cli/src/command-internal/string-slice-flag.*", "!apps/cli/src/command-internal/struct-output.*", "!apps/cli/src/command-internal/supabase-env.*", + "!apps/cli/src/command-internal/target/parent-scoped-project-ref.*", "!apps/cli/src/command-internal/tenant-*", "!apps/cli/src/command-internal/test-db.*", "!apps/cli/src/command-internal/timestamp.format.*", diff --git a/README.md b/README.md index c5ea9ae1bd..f9515f796d 100644 --- a/README.md +++ b/README.md @@ -77,12 +77,14 @@ supabase link ## Manage Your Database -Create migrations, compare schemas, and apply changes locally or to your linked project: +Create migrations, compare schemas, and apply changes to your local database. Add `--remote` to act +on your linked project instead: ```sh supabase migration new create_profiles supabase db diff supabase db push +supabase db push --remote supabase db reset ``` @@ -101,8 +103,8 @@ supabase functions deploy hello-world Generate TypeScript types from your local database or linked project: ```sh -supabase gen types --local -supabase gen types --linked +supabase gen types +supabase gen types --remote ``` ## Reference diff --git a/apps/cli-e2e/AGENTS.md b/apps/cli-e2e/AGENTS.md index 88cb932497..7cd57a5f91 100644 --- a/apps/cli-e2e/AGENTS.md +++ b/apps/cli-e2e/AGENTS.md @@ -64,7 +64,7 @@ the running tests exercise. The implications: import { testBehaviour } from "./test-context.ts"; testBehaviour("does the thing", async ({ run, projectRef, orgId, apiUrl, workspace }) => { - const result = await run(["command", "--project-ref", projectRef]); + const result = await run(["command", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("Expected output"); }); @@ -91,7 +91,7 @@ testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["command", "--project-ref", PROJECT_REF]); + const result = await run(["command", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); ``` diff --git a/apps/cli-e2e/fixtures/scenarios/db-dump-exits-non-zero-on-connection-refused-with-local/interactions.json b/apps/cli-e2e/fixtures/scenarios/db-dump-exits-non-zero-on-connection-refused-without-remote/interactions.json similarity index 100% rename from apps/cli-e2e/fixtures/scenarios/db-dump-exits-non-zero-on-connection-refused-with-local/interactions.json rename to apps/cli-e2e/fixtures/scenarios/db-dump-exits-non-zero-on-connection-refused-without-remote/interactions.json diff --git a/apps/cli-e2e/fixtures/scenarios/db-lint-exits-non-zero-on-connection-refused-with-local/interactions.json b/apps/cli-e2e/fixtures/scenarios/db-lint-exits-non-zero-on-connection-refused-without-remote/interactions.json similarity index 100% rename from apps/cli-e2e/fixtures/scenarios/db-lint-exits-non-zero-on-connection-refused-with-local/interactions.json rename to apps/cli-e2e/fixtures/scenarios/db-lint-exits-non-zero-on-connection-refused-without-remote/interactions.json diff --git a/apps/cli-e2e/fixtures/scenarios/db-pull-exits-non-zero-on-connection-refused-with-local/interactions.json b/apps/cli-e2e/fixtures/scenarios/db-pull-exits-non-zero-on-connection-refused-without-remote/interactions.json similarity index 100% rename from apps/cli-e2e/fixtures/scenarios/db-pull-exits-non-zero-on-connection-refused-with-local/interactions.json rename to apps/cli-e2e/fixtures/scenarios/db-pull-exits-non-zero-on-connection-refused-without-remote/interactions.json diff --git a/apps/cli-e2e/fixtures/scenarios/db-push-local-exits-non-zero-on-connection-refused-with-local/interactions.json b/apps/cli-e2e/fixtures/scenarios/db-push-local-exits-non-zero-on-connection-refused-without-remote/interactions.json similarity index 100% rename from apps/cli-e2e/fixtures/scenarios/db-push-local-exits-non-zero-on-connection-refused-with-local/interactions.json rename to apps/cli-e2e/fixtures/scenarios/db-push-local-exits-non-zero-on-connection-refused-without-remote/interactions.json diff --git a/apps/cli-e2e/fixtures/scenarios/db-push-linked-exits-non-zero-on-connection-refused-with-linked/interactions.json b/apps/cli-e2e/fixtures/scenarios/db-push-remote-exits-non-zero-on-connection-refused-with-remote/interactions.json similarity index 100% rename from apps/cli-e2e/fixtures/scenarios/db-push-linked-exits-non-zero-on-connection-refused-with-linked/interactions.json rename to apps/cli-e2e/fixtures/scenarios/db-push-remote-exits-non-zero-on-connection-refused-with-remote/interactions.json diff --git a/apps/cli-e2e/fixtures/scenarios/db-query-linked-exits-non-zero-on-401/interactions.json b/apps/cli-e2e/fixtures/scenarios/db-query-remote-exits-non-zero-on-401/interactions.json similarity index 100% rename from apps/cli-e2e/fixtures/scenarios/db-query-linked-exits-non-zero-on-401/interactions.json rename to apps/cli-e2e/fixtures/scenarios/db-query-remote-exits-non-zero-on-401/interactions.json diff --git a/apps/cli-e2e/fixtures/scenarios/db-query-linked-exits-non-zero-on-403/interactions.json b/apps/cli-e2e/fixtures/scenarios/db-query-remote-exits-non-zero-on-403/interactions.json similarity index 100% rename from apps/cli-e2e/fixtures/scenarios/db-query-linked-exits-non-zero-on-403/interactions.json rename to apps/cli-e2e/fixtures/scenarios/db-query-remote-exits-non-zero-on-403/interactions.json diff --git a/apps/cli-e2e/fixtures/scenarios/db-query-linked-returns-json-with-output-json/interactions.json b/apps/cli-e2e/fixtures/scenarios/db-query-remote-returns-json-with-output-json/interactions.json similarity index 97% rename from apps/cli-e2e/fixtures/scenarios/db-query-linked-returns-json-with-output-json/interactions.json rename to apps/cli-e2e/fixtures/scenarios/db-query-remote-returns-json-with-output-json/interactions.json index 4ce1bb15f0..820418ae8a 100644 --- a/apps/cli-e2e/fixtures/scenarios/db-query-linked-returns-json-with-output-json/interactions.json +++ b/apps/cli-e2e/fixtures/scenarios/db-query-remote-returns-json-with-output-json/interactions.json @@ -13,7 +13,8 @@ "user-agent": "SupabaseCLI/" }, "body": { - "query": "SELECT 1" + "query": "SELECT 1", + "read_only": true } }, "response": { diff --git a/apps/cli-e2e/fixtures/scenarios/db-query-linked-returns-select-1-result-in-table-format/interactions.json b/apps/cli-e2e/fixtures/scenarios/db-query-remote-returns-select-1-result-in-table-format/interactions.json similarity index 97% rename from apps/cli-e2e/fixtures/scenarios/db-query-linked-returns-select-1-result-in-table-format/interactions.json rename to apps/cli-e2e/fixtures/scenarios/db-query-remote-returns-select-1-result-in-table-format/interactions.json index 533edf3271..37938ca646 100644 --- a/apps/cli-e2e/fixtures/scenarios/db-query-linked-returns-select-1-result-in-table-format/interactions.json +++ b/apps/cli-e2e/fixtures/scenarios/db-query-remote-returns-select-1-result-in-table-format/interactions.json @@ -13,7 +13,8 @@ "user-agent": "SupabaseCLI/" }, "body": { - "query": "SELECT 1" + "query": "SELECT 1", + "read_only": true } }, "response": { diff --git a/apps/cli-e2e/fixtures/scenarios/db-reset-exits-non-zero-on-connection-refused-with-local/interactions.json b/apps/cli-e2e/fixtures/scenarios/db-reset-exits-non-zero-on-connection-refused-without-remote/interactions.json similarity index 100% rename from apps/cli-e2e/fixtures/scenarios/db-reset-exits-non-zero-on-connection-refused-with-local/interactions.json rename to apps/cli-e2e/fixtures/scenarios/db-reset-exits-non-zero-on-connection-refused-without-remote/interactions.json diff --git a/apps/cli-e2e/fixtures/scenarios/gen-types-exits-non-zero-with-no-data-source-specified/interactions.json b/apps/cli-e2e/fixtures/scenarios/gen-types-exits-non-zero-when-the-local-stack-is-not-running/interactions.json similarity index 100% rename from apps/cli-e2e/fixtures/scenarios/gen-types-exits-non-zero-with-no-data-source-specified/interactions.json rename to apps/cli-e2e/fixtures/scenarios/gen-types-exits-non-zero-when-the-local-stack-is-not-running/interactions.json diff --git a/apps/cli-e2e/fixtures/scenarios/inspect-flags-rejects-db-url-with-local/interactions.json b/apps/cli-e2e/fixtures/scenarios/inspect-flags-rejects-remote-with-local/interactions.json similarity index 100% rename from apps/cli-e2e/fixtures/scenarios/inspect-flags-rejects-db-url-with-local/interactions.json rename to apps/cli-e2e/fixtures/scenarios/inspect-flags-rejects-remote-with-local/interactions.json diff --git a/apps/cli-e2e/fixtures/scenarios/remote-target-selection-reads-a-space-separated-remote-ref-as-remote-ref/interactions.json b/apps/cli-e2e/fixtures/scenarios/remote-target-selection-reads-a-space-separated-remote-ref-as-remote-ref/interactions.json new file mode 100644 index 0000000000..0790d147ea --- /dev/null +++ b/apps/cli-e2e/fixtures/scenarios/remote-target-selection-reads-a-space-separated-remote-ref-as-remote-ref/interactions.json @@ -0,0 +1,69 @@ +[ + { + "request": { + "method": "GET", + "path": "/v1/projects/replayfixtureprojref/advisors/security", + "query": {}, + "headers": { + "accept-encoding": "gzip", + "authorization": "Bearer __ACCESS_TOKEN__", + "host": "localhost:__PORT__", + "user-agent": "SupabaseCLI/" + }, + "body": null + }, + "response": { + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "x-gotrue-id": "__UUID__", + "x-ratelimit-limit": "120", + "x-ratelimit-remaining": "119", + "x-ratelimit-reset": "60" + }, + "body": { + "lints": [] + } + } + }, + { + "request": { + "method": "GET", + "path": "/v1/projects/replayfixtureprojref", + "query": {}, + "headers": { + "accept-encoding": "gzip", + "authorization": "Bearer __ACCESS_TOKEN__", + "host": "localhost:__PORT__", + "user-agent": "SupabaseCLI/" + }, + "body": null + }, + "response": { + "status": 200, + "headers": { + "content-type": "application/json; charset=utf-8", + "x-gotrue-id": "__UUID__", + "x-ratelimit-limit": "120", + "x-ratelimit-remaining": "116", + "x-ratelimit-reset": "54" + }, + "body": { + "id": "replayfixtureprojref", + "ref": "replayfixtureprojref", + "organization_id": "replayfixtureprojref", + "organization_slug": "replayfixtureprojref", + "name": "cli-e2e-test", + "region": "us-east-1", + "status": "ACTIVE_HEALTHY", + "database": { + "host": "db.replayfixtureprojref.supabase.red", + "version": "17.6.1.111", + "postgres_engine": "17", + "release_channel": "ga" + }, + "created_at": "2000-01-01T00:00:00Z" + } + } + } +] diff --git a/apps/cli-e2e/fixtures/scenarios/test-db-exits-non-zero-on-connection-refused-with-local/interactions.json b/apps/cli-e2e/fixtures/scenarios/remote-target-selection-rejects-remote-url-on-a-hosted-command/interactions.json similarity index 100% rename from apps/cli-e2e/fixtures/scenarios/test-db-exits-non-zero-on-connection-refused-with-local/interactions.json rename to apps/cli-e2e/fixtures/scenarios/remote-target-selection-rejects-remote-url-on-a-hosted-command/interactions.json diff --git a/apps/cli-e2e/fixtures/scenarios/test-db-exits-non-zero-on-connection-refused-without-remote/interactions.json b/apps/cli-e2e/fixtures/scenarios/test-db-exits-non-zero-on-connection-refused-without-remote/interactions.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/apps/cli-e2e/fixtures/scenarios/test-db-exits-non-zero-on-connection-refused-without-remote/interactions.json @@ -0,0 +1 @@ +[] diff --git a/apps/cli-e2e/src/tests/advanced-platform-features.e2e.test.ts b/apps/cli-e2e/src/tests/advanced-platform-features.e2e.test.ts index 052284a2f0..b3a9e76493 100644 --- a/apps/cli-e2e/src/tests/advanced-platform-features.e2e.test.ts +++ b/apps/cli-e2e/src/tests/advanced-platform-features.e2e.test.ts @@ -9,8 +9,7 @@ describe("postgres-config", () => { "postgres-config", "get", "--experimental", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).not.toBe(""); @@ -25,8 +24,7 @@ describe("postgres-config", () => { "--experimental", "--output", "json", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); const parsed = JSON.parse(result.stdout); @@ -41,8 +39,7 @@ describe("postgres-config", () => { "get", "--experimental", "--debug", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stderr).toMatch(/HTTP.*GET:/); @@ -58,8 +55,7 @@ describe("postgres-config", () => { "postgres-config", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -75,8 +71,7 @@ describe("postgres-config", () => { "postgres-config", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); @@ -92,8 +87,7 @@ describe("postgres-config", () => { "postgres-config", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -109,8 +103,7 @@ describe("postgres-config", () => { "postgres-config", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -126,8 +119,7 @@ describe("postgres-config", () => { "postgres-config", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -142,8 +134,8 @@ describe("postgres-config", () => { "--experimental", "--config", "max_connections=200", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -157,8 +149,8 @@ describe("postgres-config", () => { "max_connections=200", "--config", "shared_buffers=256MB", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -173,8 +165,8 @@ describe("postgres-config", () => { "--config", "max_connections=200", "--replace-existing-overrides", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }, @@ -190,8 +182,8 @@ describe("postgres-config", () => { "--config", "max_connections=200", "--no-restart", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }, @@ -209,8 +201,8 @@ describe("postgres-config", () => { "--experimental", "--config", "max_connections=200", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -228,8 +220,8 @@ describe("postgres-config", () => { "--experimental", "--config", "max_connections=200", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -250,8 +242,8 @@ describe("postgres-config", () => { "--experimental", "--config", "invalid_key=value", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("unrecognized config key"); @@ -269,8 +261,8 @@ describe("postgres-config", () => { "--experimental", "--config", "max_connections=200", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -288,8 +280,8 @@ describe("postgres-config", () => { "--experimental", "--config", "max_connections=200", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -304,8 +296,8 @@ describe("postgres-config", () => { "--experimental", "--config", "max_connections", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -319,8 +311,8 @@ describe("postgres-config", () => { "max_connections", "--config", "shared_buffers", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -335,8 +327,8 @@ describe("postgres-config", () => { "--config", "max_connections", "--no-restart", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }, @@ -354,8 +346,8 @@ describe("postgres-config", () => { "--experimental", "--config", "max_connections", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -373,8 +365,8 @@ describe("postgres-config", () => { "--experimental", "--config", "max_connections", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -392,8 +384,8 @@ describe("postgres-config", () => { "--experimental", "--config", "max_connections", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -411,8 +403,8 @@ describe("postgres-config", () => { "--experimental", "--config", "max_connections", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -427,8 +419,7 @@ describe("vanity-subdomains", () => { "vanity-subdomains", "get", "--experimental", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).not.toBe(""); @@ -443,8 +434,7 @@ describe("vanity-subdomains", () => { "--experimental", "--output", "json", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); const parsed = JSON.parse(result.stdout); @@ -458,8 +448,7 @@ describe("vanity-subdomains", () => { "get", "--experimental", "--debug", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stderr).toMatch(/HTTP.*GET:/); @@ -475,8 +464,7 @@ describe("vanity-subdomains", () => { "vanity-subdomains", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -492,8 +480,7 @@ describe("vanity-subdomains", () => { "vanity-subdomains", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); @@ -509,8 +496,7 @@ describe("vanity-subdomains", () => { "vanity-subdomains", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -526,8 +512,7 @@ describe("vanity-subdomains", () => { "vanity-subdomains", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -543,8 +528,7 @@ describe("vanity-subdomains", () => { "vanity-subdomains", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -559,8 +543,7 @@ describe("vanity-subdomains", () => { "--experimental", "--desired-subdomain", "myapp", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).not.toBe(""); @@ -577,8 +560,7 @@ describe("vanity-subdomains", () => { "myapp", "--output", "json", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); const parsed = JSON.parse(result.stdout); @@ -591,8 +573,7 @@ describe("vanity-subdomains", () => { "vanity-subdomains", "check-availability", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); }); @@ -612,8 +593,7 @@ describe("vanity-subdomains", () => { "--experimental", "--desired-subdomain", "taken", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("already taken"); @@ -631,8 +611,7 @@ describe("vanity-subdomains", () => { "--experimental", "--desired-subdomain", "myapp", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -650,8 +629,7 @@ describe("vanity-subdomains", () => { "--experimental", "--desired-subdomain", "myapp", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -669,8 +647,7 @@ describe("vanity-subdomains", () => { "--experimental", "--desired-subdomain", "myapp", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -685,8 +662,8 @@ describe("vanity-subdomains", () => { "--experimental", "--desired-subdomain", "myapp", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("Activated vanity subdomain"); @@ -707,8 +684,8 @@ describe("vanity-subdomains", () => { "--experimental", "--desired-subdomain", "taken", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("already taken"); @@ -726,8 +703,8 @@ describe("vanity-subdomains", () => { "--experimental", "--desired-subdomain", "myapp", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -745,8 +722,8 @@ describe("vanity-subdomains", () => { "--experimental", "--desired-subdomain", "myapp", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -764,8 +741,8 @@ describe("vanity-subdomains", () => { "--experimental", "--desired-subdomain", "myapp", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -783,8 +760,8 @@ describe("vanity-subdomains", () => { "--experimental", "--desired-subdomain", "myapp", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -797,8 +774,8 @@ describe("vanity-subdomains", () => { "vanity-subdomains", "delete", "--experimental", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stderr).toContain("Deleted vanity subdomain"); @@ -814,8 +791,8 @@ describe("vanity-subdomains", () => { "vanity-subdomains", "delete", "--experimental", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -831,8 +808,8 @@ describe("vanity-subdomains", () => { "vanity-subdomains", "delete", "--experimental", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -848,8 +825,8 @@ describe("vanity-subdomains", () => { "vanity-subdomains", "delete", "--experimental", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -865,8 +842,8 @@ describe("vanity-subdomains", () => { "vanity-subdomains", "delete", "--experimental", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -877,7 +854,7 @@ describe("vanity-subdomains", () => { describe("encryption", () => { describe("encryption:get-root-key", () => { testBehaviour("renders root encryption key", async ({ run, projectRef }) => { - const result = await run(["encryption", "get-root-key", "--project-ref", projectRef]); + const result = await run(["encryption", "get-root-key", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).not.toBe(""); }); @@ -888,20 +865,13 @@ describe("encryption", () => { "get-root-key", "--output", "json", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); testBehaviour("--debug shows HTTP trace", async ({ run, projectRef }) => { - const result = await run([ - "encryption", - "get-root-key", - "--debug", - "--project-ref", - projectRef, - ]); + const result = await run(["encryption", "get-root-key", "--debug", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stderr).toMatch(/HTTP.*GET:/); }); @@ -912,7 +882,7 @@ describe("encryption", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["encryption", "get-root-key", "--project-ref", PROJECT_REF]); + const result = await run(["encryption", "get-root-key", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -923,7 +893,7 @@ describe("encryption", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["encryption", "get-root-key", "--project-ref", PROJECT_REF]); + const result = await run(["encryption", "get-root-key", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); }); @@ -934,7 +904,7 @@ describe("encryption", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), }); - const result = await run(["encryption", "get-root-key", "--project-ref", PROJECT_REF]); + const result = await run(["encryption", "get-root-key", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); }); @@ -945,7 +915,7 @@ describe("encryption", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["encryption", "get-root-key", "--project-ref", PROJECT_REF]); + const result = await run(["encryption", "get-root-key", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); }); @@ -956,7 +926,7 @@ describe("encryption", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["encryption", "get-root-key", "--project-ref", PROJECT_REF]); + const result = await run(["encryption", "get-root-key", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); }); @@ -964,7 +934,12 @@ describe("encryption", () => { describe("encryption:update-root-key", () => { testBehaviour.skipIf(isRecording)("rotates the vault root key", async ({ run, projectRef }) => { - const result = await run(["encryption", "update-root-key", "--project-ref", projectRef]); + const result = await run([ + "encryption", + "update-root-key", + "--yes", + `--remote=${projectRef}`, + ]); expect(result.exitCode).toBe(0); }); @@ -974,7 +949,12 @@ describe("encryption", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["encryption", "update-root-key", "--project-ref", PROJECT_REF]); + const result = await run([ + "encryption", + "update-root-key", + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -985,7 +965,12 @@ describe("encryption", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["encryption", "update-root-key", "--project-ref", PROJECT_REF]); + const result = await run([ + "encryption", + "update-root-key", + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); }); @@ -996,7 +981,12 @@ describe("encryption", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), }); - const result = await run(["encryption", "update-root-key", "--project-ref", PROJECT_REF]); + const result = await run([ + "encryption", + "update-root-key", + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); }); @@ -1007,7 +997,12 @@ describe("encryption", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["encryption", "update-root-key", "--project-ref", PROJECT_REF]); + const result = await run([ + "encryption", + "update-root-key", + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); }); @@ -1018,7 +1013,12 @@ describe("encryption", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["encryption", "update-root-key", "--project-ref", PROJECT_REF]); + const result = await run([ + "encryption", + "update-root-key", + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); }); @@ -1028,7 +1028,7 @@ describe("encryption", () => { describe("backups", () => { describe("backups:list", () => { testBehaviour("renders backup table with REGION column", async ({ run, projectRef }) => { - const result = await run(["backups", "list", "--project-ref", projectRef]); + const result = await run(["backups", "list", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("REGION"); }); @@ -1036,14 +1036,7 @@ describe("backups", () => { testBehaviour( "returns backup response as JSON with --output json", async ({ run, projectRef }) => { - const result = await run([ - "backups", - "list", - "--output", - "json", - "--project-ref", - projectRef, - ]); + const result = await run(["backups", "list", "--output", "json", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); const parsed = JSON.parse(result.stdout); expect(parsed).toHaveProperty("region"); @@ -1051,7 +1044,7 @@ describe("backups", () => { ); testBehaviour("--debug shows HTTP trace", async ({ run, projectRef }) => { - const result = await run(["backups", "list", "--debug", "--project-ref", projectRef]); + const result = await run(["backups", "list", "--debug", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stderr).toMatch(/HTTP.*GET:/); }); @@ -1062,7 +1055,7 @@ describe("backups", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["backups", "list", "--project-ref", PROJECT_REF]); + const result = await run(["backups", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -1073,7 +1066,7 @@ describe("backups", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["backups", "list", "--project-ref", PROJECT_REF]); + const result = await run(["backups", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); }); @@ -1084,7 +1077,7 @@ describe("backups", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), }); - const result = await run(["backups", "list", "--project-ref", PROJECT_REF]); + const result = await run(["backups", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); }); @@ -1095,7 +1088,7 @@ describe("backups", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["backups", "list", "--project-ref", PROJECT_REF]); + const result = await run(["backups", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); }); @@ -1106,7 +1099,7 @@ describe("backups", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["backups", "list", "--project-ref", PROJECT_REF]); + const result = await run(["backups", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); }); @@ -1121,8 +1114,8 @@ describe("backups", () => { "restore", "-t", String(BACKUP_TIMESTAMP), - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }, @@ -1137,7 +1130,14 @@ describe("backups", () => { body: { message: "recovery time target is out of range" }, }), }); - const result = await run(["backups", "restore", "-t", "0", "--project-ref", PROJECT_REF]); + const result = await run([ + "backups", + "restore", + "-t", + "0", + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("out of range"); }); @@ -1153,8 +1153,8 @@ describe("backups", () => { "restore", "-t", String(BACKUP_TIMESTAMP), - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -1171,8 +1171,8 @@ describe("backups", () => { "restore", "-t", String(BACKUP_TIMESTAMP), - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -1189,8 +1189,8 @@ describe("backups", () => { "restore", "-t", String(BACKUP_TIMESTAMP), - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -1207,8 +1207,8 @@ describe("backups", () => { "restore", "-t", String(BACKUP_TIMESTAMP), - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -1219,7 +1219,7 @@ describe("backups", () => { describe("snippets", () => { describe("snippets:list", () => { testBehaviour("renders snippet table with ID and NAME columns", async ({ run, projectRef }) => { - const result = await run(["snippets", "list", "--project-ref", projectRef]); + const result = await run(["snippets", "list", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("ID"); expect(result.stdout).toContain("NAME"); @@ -1233,8 +1233,7 @@ describe("snippets", () => { "list", "--output", "json", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); const parsed = JSON.parse(result.stdout); @@ -1243,7 +1242,7 @@ describe("snippets", () => { ); testBehaviour("--debug shows HTTP trace", async ({ run, projectRef }) => { - const result = await run(["snippets", "list", "--debug", "--project-ref", projectRef]); + const result = await run(["snippets", "list", "--debug", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stderr).toMatch(/HTTP.*GET:/); }); @@ -1254,7 +1253,7 @@ describe("snippets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["snippets", "list", "--project-ref", PROJECT_REF]); + const result = await run(["snippets", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -1265,7 +1264,7 @@ describe("snippets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["snippets", "list", "--project-ref", PROJECT_REF]); + const result = await run(["snippets", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); }); @@ -1276,7 +1275,7 @@ describe("snippets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), }); - const result = await run(["snippets", "list", "--project-ref", PROJECT_REF]); + const result = await run(["snippets", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); }); @@ -1287,7 +1286,7 @@ describe("snippets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["snippets", "list", "--project-ref", PROJECT_REF]); + const result = await run(["snippets", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); }); @@ -1298,7 +1297,7 @@ describe("snippets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["snippets", "list", "--project-ref", PROJECT_REF]); + const result = await run(["snippets", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); }); @@ -1308,7 +1307,7 @@ describe("snippets", () => { testBehaviour.skipIf(isRecording)( "prints SQL content to stdout", async ({ run, projectRef }) => { - const result = await run(["snippets", "download", SNIPPET_ID, "--project-ref", projectRef]); + const result = await run(["snippets", "download", SNIPPET_ID, `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).not.toBe(""); }, @@ -1320,7 +1319,7 @@ describe("snippets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "Snippet not found" } }), }); - const result = await run(["snippets", "download", SNIPPET_ID, "--project-ref", PROJECT_REF]); + const result = await run(["snippets", "download", SNIPPET_ID, `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("not found"); }); @@ -1331,7 +1330,7 @@ describe("snippets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["snippets", "download", SNIPPET_ID, "--project-ref", PROJECT_REF]); + const result = await run(["snippets", "download", SNIPPET_ID, `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -1342,7 +1341,7 @@ describe("snippets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["snippets", "download", SNIPPET_ID, "--project-ref", PROJECT_REF]); + const result = await run(["snippets", "download", SNIPPET_ID, `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); }); @@ -1353,7 +1352,7 @@ describe("snippets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["snippets", "download", SNIPPET_ID, "--project-ref", PROJECT_REF]); + const result = await run(["snippets", "download", SNIPPET_ID, `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); }); diff --git a/apps/cli-e2e/src/tests/branches.e2e.test.ts b/apps/cli-e2e/src/tests/branches.e2e.test.ts index a956c1b577..f979199dba 100644 --- a/apps/cli-e2e/src/tests/branches.e2e.test.ts +++ b/apps/cli-e2e/src/tests/branches.e2e.test.ts @@ -7,7 +7,7 @@ const BRANCH_NAME = "my-branch"; describe("branches", () => { describe("branches:list", () => { testBehaviour("renders fixture data in output", async ({ run, projectRef }) => { - const result = await run(["branches", "list", "--project-ref", projectRef]); + const result = await run(["branches", "list", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("NAME"); expect(result.stdout).toContain("STATUS"); @@ -21,8 +21,7 @@ describe("branches", () => { "list", "--output", "json", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); const parsed = JSON.parse(result.stdout) as unknown[]; @@ -31,7 +30,7 @@ describe("branches", () => { ); testBehaviour("includes debug output with --debug", async ({ run, projectRef }) => { - const result = await run(["branches", "list", "--debug", "--project-ref", projectRef]); + const result = await run(["branches", "list", "--debug", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stderr).toMatch(/HTTP.*GET:/); }); @@ -42,7 +41,7 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["branches", "list", "--project-ref", PROJECT_REF]); + const result = await run(["branches", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -53,7 +52,7 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["branches", "list", "--project-ref", PROJECT_REF]); + const result = await run(["branches", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); }); @@ -64,7 +63,7 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["branches", "list", "--project-ref", PROJECT_REF]); + const result = await run(["branches", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); }); @@ -75,7 +74,7 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["branches", "list", "--project-ref", PROJECT_REF]); + const result = await run(["branches", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); }); @@ -83,7 +82,13 @@ describe("branches", () => { describe("branches:create", () => { testBehaviour.skipIf(isRecording)("creates ephemeral branch", async ({ run, projectRef }) => { - const result = await run(["branches", "create", BRANCH_NAME, "--project-ref", projectRef]); + const result = await run([ + "branches", + "create", + BRANCH_NAME, + "--yes", + `--remote=${projectRef}`, + ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("Created preview branch:"); }); @@ -94,8 +99,8 @@ describe("branches", () => { "create", BRANCH_NAME, "--persistent", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("Created preview branch:"); @@ -107,8 +112,8 @@ describe("branches", () => { "create", BRANCH_NAME, "--with-data", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("Created preview branch:"); @@ -120,7 +125,13 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 409, body: { message: "Branch name already in use" } }), }); - const result = await run(["branches", "create", BRANCH_NAME, "--project-ref", PROJECT_REF]); + const result = await run([ + "branches", + "create", + BRANCH_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Branch name already in use"); }); @@ -134,7 +145,13 @@ describe("branches", () => { body: { message: "Preview branching is not enabled" }, }), }); - const result = await run(["branches", "create", BRANCH_NAME, "--project-ref", PROJECT_REF]); + const result = await run([ + "branches", + "create", + BRANCH_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Preview branching is not enabled"); }); @@ -151,8 +168,8 @@ describe("branches", () => { BRANCH_NAME, "--region", "invalid-region", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("--region"); @@ -164,7 +181,13 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["branches", "create", BRANCH_NAME, "--project-ref", PROJECT_REF]); + const result = await run([ + "branches", + "create", + BRANCH_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -175,7 +198,13 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["branches", "create", BRANCH_NAME, "--project-ref", PROJECT_REF]); + const result = await run([ + "branches", + "create", + BRANCH_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); }); @@ -186,7 +215,13 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["branches", "create", BRANCH_NAME, "--project-ref", PROJECT_REF]); + const result = await run([ + "branches", + "create", + BRANCH_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); }); @@ -196,7 +231,7 @@ describe("branches", () => { testBehaviour.skipIf(isRecording)( "returns single branch details", async ({ run, projectRef }) => { - const result = await run(["branches", "get", BRANCH_NAME, "--project-ref", projectRef]); + const result = await run(["branches", "get", BRANCH_NAME, `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain(BRANCH_NAME); }, @@ -211,8 +246,7 @@ describe("branches", () => { BRANCH_NAME, "--output", "json", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); const parsed = JSON.parse(result.stdout) as Record; @@ -226,7 +260,7 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "Branch not found" } }), }); - const result = await run(["branches", "get", "nonexistent", "--project-ref", PROJECT_REF]); + const result = await run(["branches", "get", "nonexistent", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Branch not found"); }); @@ -237,7 +271,7 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["branches", "get", BRANCH_NAME, "--project-ref", PROJECT_REF]); + const result = await run(["branches", "get", BRANCH_NAME, `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -251,8 +285,8 @@ describe("branches", () => { BRANCH_NAME, "--name", "renamed-branch", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stderr).toContain("Updated preview branch:"); @@ -267,8 +301,8 @@ describe("branches", () => { BRANCH_NAME, "--git-branch", "feature/new", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stderr).toContain("Updated preview branch:"); @@ -287,8 +321,8 @@ describe("branches", () => { "nonexistent", "--name", "new-name", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Branch not found"); @@ -306,8 +340,8 @@ describe("branches", () => { BRANCH_NAME, "--name", "new-name", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -316,7 +350,13 @@ describe("branches", () => { describe("branches:pause", () => { testBehaviour.skipIf(isRecording)("pauses branch successfully", async ({ run, projectRef }) => { - const result = await run(["branches", "pause", BRANCH_NAME, "--project-ref", projectRef]); + const result = await run([ + "branches", + "pause", + BRANCH_NAME, + "--yes", + `--remote=${projectRef}`, + ]); expect(result.exitCode).toBe(0); }); @@ -326,7 +366,13 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "Branch not found" } }), }); - const result = await run(["branches", "pause", "nonexistent", "--project-ref", PROJECT_REF]); + const result = await run([ + "branches", + "pause", + "nonexistent", + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Branch not found"); }); @@ -337,7 +383,13 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["branches", "pause", BRANCH_NAME, "--project-ref", PROJECT_REF]); + const result = await run([ + "branches", + "pause", + BRANCH_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -347,7 +399,13 @@ describe("branches", () => { testBehaviour.skipIf(isRecording)( "unpauses branch successfully", async ({ run, projectRef }) => { - const result = await run(["branches", "unpause", BRANCH_NAME, "--project-ref", projectRef]); + const result = await run([ + "branches", + "unpause", + BRANCH_NAME, + "--yes", + `--remote=${projectRef}`, + ]); expect(result.exitCode).toBe(0); }, ); @@ -358,7 +416,13 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["branches", "unpause", BRANCH_NAME, "--project-ref", PROJECT_REF]); + const result = await run([ + "branches", + "unpause", + BRANCH_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -368,7 +432,13 @@ describe("branches", () => { testBehaviour.skipIf(isRecording)( "deletes branch successfully", async ({ run, projectRef }) => { - const result = await run(["branches", "delete", BRANCH_NAME, "--project-ref", projectRef]); + const result = await run([ + "branches", + "delete", + BRANCH_NAME, + "--yes", + `--remote=${projectRef}`, + ]); expect(result.exitCode).toBe(0); expect(result.stderr).toContain("Deleted preview branch:"); }, @@ -380,7 +450,13 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "Branch not found" } }), }); - const result = await run(["branches", "delete", "nonexistent", "--project-ref", PROJECT_REF]); + const result = await run([ + "branches", + "delete", + "nonexistent", + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Branch not found"); }); @@ -391,7 +467,13 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["branches", "delete", BRANCH_NAME, "--project-ref", PROJECT_REF]); + const result = await run([ + "branches", + "delete", + BRANCH_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -399,7 +481,7 @@ describe("branches", () => { describe("branches:disable", () => { testBehaviour.skipIf(isRecording)("disables preview branching", async ({ run, projectRef }) => { - const result = await run(["branches", "disable", "--project-ref", projectRef]); + const result = await run(["branches", "disable", "--yes", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("Disabled preview branching for project:"); }); @@ -410,7 +492,7 @@ describe("branches", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["branches", "disable", "--project-ref", PROJECT_REF]); + const result = await run(["branches", "disable", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); diff --git a/apps/cli-e2e/src/tests/config-push.e2e.test.ts b/apps/cli-e2e/src/tests/config-push.e2e.test.ts index 617c6158cc..3be38add6e 100644 --- a/apps/cli-e2e/src/tests/config-push.e2e.test.ts +++ b/apps/cli-e2e/src/tests/config-push.e2e.test.ts @@ -59,13 +59,13 @@ enabled = true describe("config push", () => { testBehaviour("reconciles every section against the remote", async ({ run, workspace }) => { writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", "--project-ref", PROJECT_REF]); + const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).toBe(0); }); testBehaviour("emits HTTP trace with --debug", async ({ run, workspace }) => { writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", "--project-ref", PROJECT_REF, "--debug"]); + const result = await run(["config", "push", "--yes", "--debug", `--remote=${PROJECT_REF}`]); expect(result.exitCode).toBe(0); expect(result.stderr).toMatch(/HTTP.*GET:/); }); @@ -81,7 +81,7 @@ describe("config push", () => { }); writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", "--project-ref", PROJECT_REF]); + const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("401"); }); @@ -101,7 +101,7 @@ describe("config push", () => { }); writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", "--project-ref", PROJECT_REF]); + const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("403"); }, @@ -118,7 +118,7 @@ describe("config push", () => { }); writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", "--project-ref", PROJECT_REF]); + const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("404"); }); @@ -131,7 +131,7 @@ describe("config push", () => { }); writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", "--project-ref", PROJECT_REF]); + const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("409"); }); @@ -149,7 +149,7 @@ describe("config push", () => { }); writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", "--project-ref", PROJECT_REF]); + const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("422"); expect(result.stderr).toContain("max_rows"); @@ -166,7 +166,7 @@ describe("config push", () => { }); writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", "--project-ref", PROJECT_REF]); + const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("429"); }); @@ -182,7 +182,7 @@ describe("config push", () => { }); writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", "--project-ref", PROJECT_REF]); + const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("500"); }); @@ -195,7 +195,7 @@ describe("config push", () => { }); writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", "--project-ref", PROJECT_REF]); + const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("502"); }); diff --git a/apps/cli-e2e/src/tests/config-toml.e2e.test.ts b/apps/cli-e2e/src/tests/config-toml.e2e.test.ts index e21e572773..5275c838b8 100644 --- a/apps/cli-e2e/src/tests/config-toml.e2e.test.ts +++ b/apps/cli-e2e/src/tests/config-toml.e2e.test.ts @@ -46,7 +46,7 @@ describe("env-in-config-toml", () => { body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["secrets", "list", "--project-ref", PROJECT_REF], { + const result = await run(["secrets", "list", `--remote=${PROJECT_REF}`], { env: ENV_PORTS, }); diff --git a/apps/cli-e2e/src/tests/database-core.e2e.test.ts b/apps/cli-e2e/src/tests/database-core.e2e.test.ts index 0adcb9b5ad..1a4f575c59 100644 --- a/apps/cli-e2e/src/tests/database-core.e2e.test.ts +++ b/apps/cli-e2e/src/tests/database-core.e2e.test.ts @@ -1,23 +1,9 @@ import { mkdirSync, readdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect } from "vitest"; +import { linkProject } from "./link-project.ts"; import { testBehaviour } from "./test-context.ts"; -/** Writes .supabase/.temp/project-ref and a stub pooler-url so --linked - * commands resolve a connection without a live postgres TCP probe (nothing - * listens on db.{ref}.localhost:5432 in the test harness); combined with - * SUPABASE_DB_PASSWORD (set in harness.ts), the command reaches its handler - * and makes the Management API call under test. */ -function linkProject(dir: string, ref: string): void { - const tempDir = join(dir, "supabase", ".temp"); - mkdirSync(tempDir, { recursive: true }); - writeFileSync(join(tempDir, "project-ref"), ref); - writeFileSync( - join(tempDir, "pooler-url"), - `postgresql://postgres.${ref}:[YOUR-PASSWORD]@aws-0-eu-central-1.pooler.supabase.com:6543/postgres`, - ); -} - const TEST_MIGRATION_SQL = "CREATE TABLE IF NOT EXISTS e2e_test_table (id bigint generated always as identity primary key);"; @@ -32,7 +18,7 @@ describe("db advisors", () => { describe("db advisors:security", () => { testBehaviour("returns security advisors", async ({ run, projectRef, workspace }) => { linkProject(workspace.path, projectRef); - const result = await run(["db", "advisors", "--linked", "--type", "security"]); + const result = await run(["db", "advisors", "--type", "security", "--remote"]); expect(result.exitCode).toBe(0); expect(result.stdout.trim()).toBe(""); expect(result.stderr).toContain("No issues found"); @@ -45,11 +31,11 @@ describe("db advisors", () => { const result = await run([ "db", "advisors", - "--linked", "--type", "security", "--fail-on", "error", + "--remote", ]); expect(result.exitCode).toBe(0); }, @@ -62,7 +48,7 @@ describe("db advisors", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["db", "advisors", "--linked", "--type", "security"]); + const result = await run(["db", "advisors", "--type", "security", "--remote"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -74,7 +60,7 @@ describe("db advisors", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["db", "advisors", "--linked", "--type", "security"]); + const result = await run(["db", "advisors", "--type", "security", "--remote"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); }); @@ -86,7 +72,7 @@ describe("db advisors", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["db", "advisors", "--linked", "--type", "security"]); + const result = await run(["db", "advisors", "--type", "security", "--remote"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); }); @@ -98,7 +84,7 @@ describe("db advisors", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["db", "advisors", "--linked", "--type", "security"]); + const result = await run(["db", "advisors", "--type", "security", "--remote"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); }); @@ -107,7 +93,7 @@ describe("db advisors", () => { describe("db advisors:performance", () => { testBehaviour("returns performance advisors", async ({ run, projectRef, workspace }) => { linkProject(workspace.path, projectRef); - const result = await run(["db", "advisors", "--linked", "--type", "performance"]); + const result = await run(["db", "advisors", "--type", "performance", "--remote"]); expect(result.exitCode).toBe(0); if (result.stdout.trim()) { const parsed = JSON.parse(result.stdout) as unknown[]; @@ -121,19 +107,19 @@ describe("db advisors", () => { describe("db advisors:all", () => { testBehaviour("returns advisors with --type all", async ({ run, projectRef, workspace }) => { linkProject(workspace.path, projectRef); - const result = await run(["db", "advisors", "--linked", "--type", "all"]); + const result = await run(["db", "advisors", "--type", "all", "--remote"]); expect(result.exitCode).toBe(0); }); }); }); describe("db query", () => { - describe("db query:linked", () => { + describe("db query:remote", () => { testBehaviour( "returns SELECT 1 result in table format", async ({ run, projectRef, workspace }) => { linkProject(workspace.path, projectRef); - const result = await run(["db", "query", "--linked", "SELECT 1"]); + const result = await run(["db", "query", "SELECT 1", "--read-only", "--remote"]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("1"); }, @@ -141,7 +127,15 @@ describe("db query", () => { testBehaviour("returns JSON with --output json", async ({ run, projectRef, workspace }) => { linkProject(workspace.path, projectRef); - const result = await run(["db", "query", "--linked", "--output", "json", "SELECT 1"]); + const result = await run([ + "db", + "query", + "--output", + "json", + "SELECT 1", + "--read-only", + "--remote", + ]); expect(result.exitCode).toBe(0); const parsed = JSON.parse(result.stdout) as unknown; // In agent mode (CLAUDECODE env set) the output is wrapped in {warning, boundary, rows}. @@ -158,7 +152,7 @@ describe("db query", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["db", "query", "--linked", "SELECT 1"]); + const result = await run(["db", "query", "SELECT 1", "--read-only", "--remote"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -170,7 +164,7 @@ describe("db query", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["db", "query", "--linked", "SELECT 1"]); + const result = await run(["db", "query", "SELECT 1", "--read-only", "--remote"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); }); @@ -184,7 +178,7 @@ describe("db push", () => { async ({ run, projectRef, workspace }) => { linkProject(workspace.path, projectRef); seedMigration(workspace.path); - const result = await run(["db", "push", "--dry-run"]); + const result = await run(["db", "push", "--dry-run", "--remote"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("connect"); }, @@ -192,19 +186,19 @@ describe("db push", () => { }); describe("db push:local", () => { - testBehaviour("exits non-zero on connection refused with --local", async ({ run }) => { - const result = await run(["db", "push", "--local"]); + testBehaviour("exits non-zero on connection refused without --remote", async ({ run }) => { + const result = await run(["db", "push"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("connect"); }); }); - describe("db push:linked", () => { + describe("db push:remote", () => { testBehaviour( - "exits non-zero on connection refused with --linked", + "exits non-zero on connection refused with --remote", async ({ run, projectRef, workspace }) => { linkProject(workspace.path, projectRef); - const result = await run(["db", "push"]); + const result = await run(["db", "push", "--remote"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("connect"); }, @@ -213,38 +207,38 @@ describe("db push", () => { }); describe("db pull", () => { - testBehaviour("exits non-zero on connection refused with --local", async ({ run }) => { - const result = await run(["db", "pull", "--local"]); + testBehaviour("exits non-zero on connection refused without --remote", async ({ run }) => { + const result = await run(["db", "pull"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("connect"); }); }); describe("db lint", () => { - testBehaviour("exits non-zero on connection refused with --local", async ({ run }) => { - const result = await run(["db", "lint", "--local"]); + testBehaviour("exits non-zero on connection refused without --remote", async ({ run }) => { + const result = await run(["db", "lint"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("connect"); }); }); describe("db dump", () => { - testBehaviour("exits non-zero on connection refused with --local", async ({ run }) => { - const result = await run(["db", "dump", "--local"]); + testBehaviour("exits non-zero on connection refused without --remote", async ({ run }) => { + const result = await run(["db", "dump"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("connect"); }); testBehaviour("exits non-zero when --role-only and --data-only are both set", async ({ run }) => { - const result = await run(["db", "dump", "--local", "--role-only", "--data-only"]); + const result = await run(["db", "dump", "--role-only", "--data-only"]); expect(result.exitCode).not.toBe(0); expect(result.stderr.toLowerCase()).toMatch(/role.only|data.only|mutually exclusive/); }); }); describe("db reset", () => { - testBehaviour("exits non-zero on connection refused with --local", async ({ run }) => { - const result = await run(["db", "reset", "--local"]); + testBehaviour("exits non-zero on connection refused without --remote", async ({ run }) => { + const result = await run(["db", "reset"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toMatch(/connect|not running/i); }); @@ -268,8 +262,8 @@ describe("test new", () => { }); describe("test db", () => { - testBehaviour("exits non-zero on connection refused with --local", async ({ run }) => { - const result = await run(["test", "db", "--local"]); + testBehaviour("exits non-zero on connection refused without --remote", async ({ run }) => { + const result = await run(["test", "db"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("connect"); }); diff --git a/apps/cli-e2e/src/tests/domains.e2e.test.ts b/apps/cli-e2e/src/tests/domains.e2e.test.ts index 72cb5f37bc..e2244691e6 100644 --- a/apps/cli-e2e/src/tests/domains.e2e.test.ts +++ b/apps/cli-e2e/src/tests/domains.e2e.test.ts @@ -19,21 +19,21 @@ describe("domains", () => { }), }); - const result = await run(["domains", "get", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("400"); }); testBehaviour.skipIf(isRecording)("no custom domain", async ({ run }) => { - const result = await run(["domains", "get", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("400"); }); testBehaviour.skipIf(isRecording)("pending verification", async ({ run }) => { - const result = await run(["domains", "get", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); expect(result.exitCode).toBe(0); expect(result.stderr).toContain( @@ -42,7 +42,7 @@ describe("domains", () => { }); testBehaviour.skipIf(isRecording)("verification completed", async ({ run }) => { - const result = await run(["domains", "get", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); expect(result.exitCode).toBe(0); expect(result.stderr).toContain( @@ -51,14 +51,7 @@ describe("domains", () => { }); testBehaviour.skipIf(isRecording)("domain activated", async ({ run }) => { - const result = await run([ - "domains", - "get", - "--project-ref", - PROJECT_REF, - "--output", - "json", - ]); + const result = await run(["domains", "get", "--output", "json", `--remote=${PROJECT_REF}`]); expect(result.exitCode).toBe(0); @@ -98,7 +91,7 @@ describe("domains", () => { }), }); - const result = await run(["domains", "get", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("403"); }); @@ -113,7 +106,7 @@ describe("domains", () => { }), }); - const result = await run(["domains", "get", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("401"); }); @@ -128,7 +121,7 @@ describe("domains", () => { }), }); - const result = await run(["domains", "get", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("429"); }); @@ -143,7 +136,7 @@ describe("domains", () => { }), }); - const result = await run(["domains", "get", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("500"); @@ -159,7 +152,7 @@ describe("domains", () => { }), }); - const result = await run(["domains", "get", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("502"); @@ -168,21 +161,21 @@ describe("domains", () => { describe("domains:reverify", () => { testBehaviour("custom domain disabled", async ({ run }) => { - const result = await run(["domains", "reverify", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "reverify", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("400"); }); testBehaviour.skipIf(isRecording)("no custom domain", async ({ run }) => { - const result = await run(["domains", "reverify", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "reverify", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("400"); }); testBehaviour.skipIf(isRecording)("pending verification", async ({ run }) => { - const result = await run(["domains", "reverify", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "reverify", `--remote=${PROJECT_REF}`]); expect(result.exitCode).toBe(0); expect(result.stderr).toContain( @@ -191,7 +184,7 @@ describe("domains", () => { }); testBehaviour.skipIf(isRecording)("verification completed", async ({ run }) => { - const result = await run(["domains", "reverify", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "reverify", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("400"); @@ -199,28 +192,34 @@ describe("domains", () => { }); describe("domains:activate", () => { testBehaviour("custom domain disabled", async ({ run }) => { - const result = await run(["domains", "activate", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "activate", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("400"); }); testBehaviour.skipIf(isRecording)("no custom domain", async ({ run }) => { - const result = await run(["domains", "activate", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "activate", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("400"); }); testBehaviour.skipIf(isRecording)("pending verification", async ({ run }) => { - const result = await run(["domains", "activate", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "activate", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("400"); }); testBehaviour.skipIf(isRecording)("pending verification in debug mode", async ({ run }) => { - const result = await run(["domains", "activate", "--project-ref", PROJECT_REF, "--debug"]); + const result = await run([ + "domains", + "activate", + "--debug", + "--yes", + `--remote=${PROJECT_REF}`, + ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toMatch(/HTTP.*POST:/); @@ -228,7 +227,7 @@ describe("domains", () => { }); testBehaviour.skipIf(isRecording)("verification completed", async ({ run }) => { - const result = await run(["domains", "activate", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "activate", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).toBe(0); expect(result.stderr).toContain(`completed`); @@ -236,7 +235,7 @@ describe("domains", () => { }); testBehaviour.skipIf(isRecording)("domain activated", async ({ run }) => { - const result = await run(["domains", "activate", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "activate", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("400"); @@ -245,21 +244,21 @@ describe("domains", () => { describe("domains:delete", () => { testBehaviour("custom domain disabled", async ({ run }) => { - const result = await run(["domains", "delete", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "delete", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("400"); }); testBehaviour.skipIf(isRecording)("no custom domain", async ({ run }) => { - const result = await run(["domains", "delete", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "delete", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("400"); }); testBehaviour.skipIf(isRecording)("pending verification", async ({ run }) => { - const result = await run(["domains", "delete", "--project-ref", PROJECT_REF]); + const result = await run(["domains", "delete", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).toBe(0); }); diff --git a/apps/cli-e2e/src/tests/functions.e2e.test.ts b/apps/cli-e2e/src/tests/functions.e2e.test.ts index bfba4a5e38..af2a08db8e 100644 --- a/apps/cli-e2e/src/tests/functions.e2e.test.ts +++ b/apps/cli-e2e/src/tests/functions.e2e.test.ts @@ -7,28 +7,21 @@ const FUNCTION_NAME = "hello-world"; describe("functions", () => { describe("functions:list", () => { testBehaviour("renders fixture data in output", async ({ run, projectRef }) => { - const result = await run(["functions", "list", "--project-ref", projectRef]); + const result = await run(["functions", "list", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("NAME"); expect(result.stdout).toContain("STATUS"); }); testBehaviour("returns json output with --output json", async ({ run, projectRef }) => { - const result = await run([ - "functions", - "list", - "--output", - "json", - "--project-ref", - projectRef, - ]); + const result = await run(["functions", "list", "--output", "json", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); const parsed = JSON.parse(result.stdout) as unknown[]; expect(Array.isArray(parsed)).toBe(true); }); testBehaviour("includes debug output with --debug", async ({ run, projectRef }) => { - const result = await run(["functions", "list", "--debug", "--project-ref", projectRef]); + const result = await run(["functions", "list", "--debug", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stderr).toMatch(/HTTP.*GET:/); }); @@ -39,7 +32,7 @@ describe("functions", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["functions", "list", "--project-ref", PROJECT_REF]); + const result = await run(["functions", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); @@ -49,7 +42,7 @@ describe("functions", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["functions", "list", "--project-ref", PROJECT_REF]); + const result = await run(["functions", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); }); @@ -60,7 +53,7 @@ describe("functions", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["functions", "list", "--project-ref", PROJECT_REF]); + const result = await run(["functions", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); }); @@ -71,7 +64,7 @@ describe("functions", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["functions", "list", "--project-ref", PROJECT_REF]); + const result = await run(["functions", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); }); @@ -93,8 +86,8 @@ describe("functions", () => { "deploy", FUNCTION_NAME, "--use-api", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -112,8 +105,8 @@ describe("functions", () => { "deploy", FUNCTION_NAME, "--use-api", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); @@ -134,8 +127,8 @@ describe("functions", () => { "deploy", FUNCTION_NAME, "--use-api", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -153,8 +146,8 @@ describe("functions", () => { "deploy", FUNCTION_NAME, "--use-api", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -169,8 +162,8 @@ describe("functions", () => { "functions", "delete", FUNCTION_NAME, - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("Deleted Function"); @@ -187,8 +180,8 @@ describe("functions", () => { "functions", "delete", FUNCTION_NAME, - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("does not exist"); @@ -204,8 +197,8 @@ describe("functions", () => { "functions", "delete", FUNCTION_NAME, - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -221,8 +214,8 @@ describe("functions", () => { "functions", "delete", FUNCTION_NAME, - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); @@ -238,8 +231,8 @@ describe("functions", () => { "functions", "delete", FUNCTION_NAME, - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -255,8 +248,8 @@ describe("functions", () => { "functions", "delete", FUNCTION_NAME, - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -272,8 +265,7 @@ describe("functions", () => { "download", FUNCTION_NAME, "--use-api", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stderr).toContain("Downloaded Function"); @@ -291,8 +283,7 @@ describe("functions", () => { "download", FUNCTION_NAME, "--use-api", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -309,8 +300,7 @@ describe("functions", () => { "download", FUNCTION_NAME, "--use-api", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); @@ -327,8 +317,7 @@ describe("functions", () => { "download", FUNCTION_NAME, "--use-api", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Function not found"); @@ -345,8 +334,7 @@ describe("functions", () => { "download", FUNCTION_NAME, "--use-api", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); diff --git a/apps/cli-e2e/src/tests/gen.e2e.test.ts b/apps/cli-e2e/src/tests/gen.e2e.test.ts index 5362a5bf12..95a24b5eec 100644 --- a/apps/cli-e2e/src/tests/gen.e2e.test.ts +++ b/apps/cli-e2e/src/tests/gen.e2e.test.ts @@ -14,7 +14,7 @@ describe("gen", () => { async ({ run, projectRef }) => { // #5212 — with CLAUDECODE=1, piped/redirected output must not include the // plugin hint (would break `gen types > file.ts` and similar captures). - const result = await run(["gen", "types", "--project-id", projectRef], { + const result = await run(["gen", "types", `--remote=${projectRef}`], { env: { CLAUDECODE: "1" }, }); expect(result.exitCode).toBe(0); @@ -28,7 +28,7 @@ describe("gen", () => { testBehaviour.skipIf(isRecording)( "includes debug output with --debug", async ({ run, projectRef }) => { - const result = await run(["gen", "types", "--debug", "--project-id", projectRef]); + const result = await run(["gen", "types", "--debug", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stderr).toMatch(/HTTP.*GET:/); }, @@ -40,7 +40,7 @@ describe("gen", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["gen", "types", "--project-id", PROJECT_REF]); + const result = await run(["gen", "types", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -51,7 +51,7 @@ describe("gen", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["gen", "types", "--project-id", PROJECT_REF]); + const result = await run(["gen", "types", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); }); @@ -62,15 +62,20 @@ describe("gen", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), }); - const result = await run(["gen", "types", "--project-id", PROJECT_REF]); + const result = await run(["gen", "types", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); }); - testBehaviour("exits non-zero with no data source specified", async ({ runNoProjectId }) => { - const result = await runNoProjectId(["gen", "types"]); - expect(result.exitCode).not.toBe(0); - }); + testBehaviour( + "exits non-zero when the local stack is not running", + async ({ runNoProjectId }) => { + const result = await runNoProjectId(["gen", "types"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Target: local stack"); + expect(result.stderr).toContain("supabase start is not running"); + }, + ); }); describe("gen:signing-key", () => { diff --git a/apps/cli-e2e/src/tests/inspect.e2e.test.ts b/apps/cli-e2e/src/tests/inspect.e2e.test.ts index 9f5014dab1..bdb9bd5311 100644 --- a/apps/cli-e2e/src/tests/inspect.e2e.test.ts +++ b/apps/cli-e2e/src/tests/inspect.e2e.test.ts @@ -37,17 +37,19 @@ const SUBCOMMANDS = [ ] as const; describe("inspect:flags", () => { - testBehaviour("rejects --db-url with --local", async ({ run, workspace, pgMockPort }) => { + testBehaviour("rejects --remote with --local", async ({ run, workspace, pgMockPort }) => { setupInspectWorkspace(workspace.path, pgMockPort); const result = await run([ "inspect", "db", "db-stats", - "--db-url", - "postgresql://postgres:postgres@localhost:5432/postgres", "--local", + "--remote=postgresql://postgres:postgres@localhost:5432/postgres", ]); expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain( + "Only one of --remote, --linked, --db-url and --local can be set; got --remote and --local.", + ); }); }); @@ -58,7 +60,7 @@ for (const { name, fixtureKey, assertValue } of SUBCOMMANDS) { async ({ run, workspace, apiUrl, pgMockPort }) => { setupInspectWorkspace(workspace.path, pgMockPort); await setPgFixture(apiUrl, fixtureKey); - const result = await run(["inspect", "db", name, "--local"]); + const result = await run(["inspect", "db", name]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain(assertValue); }, @@ -69,8 +71,7 @@ for (const { name, fixtureKey, assertValue } of SUBCOMMANDS) { "inspect", "db", name, - "--db-url", - "postgresql://postgres:postgres@localhost:1/postgres", + "--remote=postgresql://postgres:postgres@localhost:1/postgres", ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).not.toBe(""); @@ -82,7 +83,7 @@ describe("inspect:report", () => { testBehaviour("saves CSV files on success", async ({ run, workspace, pgMockPort }) => { setupInspectWorkspace(workspace.path, pgMockPort); const outDir = join(workspace.path, "report-out"); - const result = await run(["inspect", "report", "--local", "--output-dir", outDir]); + const result = await run(["inspect", "report", "--output-dir", outDir]); expect(result.exitCode).toBe(0); expect(result.stderr).toContain("Reports saved to"); const subdirs = readdirSync(outDir); @@ -97,8 +98,7 @@ describe("inspect:report", () => { const result = await run([ "inspect", "report", - "--db-url", - "postgresql://postgres:postgres@localhost:1/postgres", + "--remote=postgresql://postgres:postgres@localhost:1/postgres", ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).not.toBe(""); diff --git a/apps/cli-e2e/src/tests/link-project.ts b/apps/cli-e2e/src/tests/link-project.ts new file mode 100644 index 0000000000..513658c3c7 --- /dev/null +++ b/apps/cli-e2e/src/tests/link-project.ts @@ -0,0 +1,13 @@ +import { mkdirSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +/** Writes `project-ref` and a stub `pooler-url` so `--remote` skips the live Postgres probe. */ +export function linkProject(dir: string, ref: string): void { + const tempDir = join(dir, "supabase", ".temp"); + mkdirSync(tempDir, { recursive: true }); + writeFileSync(join(tempDir, "project-ref"), ref); + writeFileSync( + join(tempDir, "pooler-url"), + `postgresql://postgres.${ref}:[YOUR-PASSWORD]@aws-0-eu-central-1.pooler.supabase.com:6543/postgres`, + ); +} diff --git a/apps/cli-e2e/src/tests/migrations.e2e.test.ts b/apps/cli-e2e/src/tests/migrations.e2e.test.ts index 3a0c349e42..3180c38ae3 100644 --- a/apps/cli-e2e/src/tests/migrations.e2e.test.ts +++ b/apps/cli-e2e/src/tests/migrations.e2e.test.ts @@ -1,4 +1,4 @@ -import { readdirSync } from "node:fs"; +import { mkdirSync, readdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect } from "vitest"; import { testBehaviour } from "./test-context.ts"; @@ -26,7 +26,7 @@ describe("migrations", () => { describe("migration:list", () => { testBehaviour("exits non-zero on connection refused", async ({ run }) => { - const result = await run(["migration", "list", "--local"]); + const result = await run(["migration", "list"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("failed to connect"); }); @@ -34,7 +34,7 @@ describe("migrations", () => { describe("migration:up", () => { testBehaviour("exits non-zero on connection refused", async ({ run }) => { - const result = await run(["migration", "up", "--local"]); + const result = await run(["migration", "up"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("failed to connect"); }); @@ -42,13 +42,13 @@ describe("migrations", () => { describe("migration:down", () => { testBehaviour("exits non-zero on connection refused", async ({ run }) => { - const result = await run(["migration", "down", "--local"]); + const result = await run(["migration", "down"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("failed to connect"); }); testBehaviour("exits non-zero on connection refused with --last 2", async ({ run }) => { - const result = await run(["migration", "down", "--last", "2", "--local"]); + const result = await run(["migration", "down", "--last", "2"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("failed to connect"); }); @@ -56,22 +56,18 @@ describe("migrations", () => { describe("migration:repair", () => { testBehaviour("exits non-zero when --status flag is missing", async ({ run }) => { - const result = await run(["migration", "repair", "--local", "20230101000000"]); + const result = await run(["migration", "repair", "20230101000000"]); expect(result.exitCode).not.toBe(0); // A missing required flag's error prints once, without a duplicate // usage dump, and spells the flag name without its `--` prefix. expect(result.stderr).toContain('"status" not set'); }); - testBehaviour("exits non-zero on connection refused", async ({ run }) => { - const result = await run([ - "migration", - "repair", - "--status", - "applied", - "--local", - "20230101000000", - ]); + testBehaviour("exits non-zero on connection refused", async ({ run, workspace }) => { + const migrationsDir = join(workspace.path, "supabase", "migrations"); + mkdirSync(migrationsDir, { recursive: true }); + writeFileSync(join(migrationsDir, "20230101000000_x.sql"), "select 1;\n"); + const result = await run(["migration", "repair", "--status", "applied", "20230101000000"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("failed to connect"); }); @@ -79,7 +75,7 @@ describe("migrations", () => { describe("migration:squash", () => { testBehaviour("exits non-zero on connection refused", async ({ run }) => { - const result = await run(["migration", "squash", "--local"]); + const result = await run(["migration", "squash"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).not.toBe(""); }); @@ -87,7 +83,7 @@ describe("migrations", () => { describe("migration:fetch", () => { testBehaviour("exits non-zero on connection refused", async ({ run }) => { - const result = await run(["migration", "fetch", "--local"]); + const result = await run(["migration", "fetch"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("failed to connect"); }); diff --git a/apps/cli-e2e/src/tests/network-security.e2e.test.ts b/apps/cli-e2e/src/tests/network-security.e2e.test.ts index 8cff53bcc4..5e19ae3803 100644 --- a/apps/cli-e2e/src/tests/network-security.e2e.test.ts +++ b/apps/cli-e2e/src/tests/network-security.e2e.test.ts @@ -5,13 +5,7 @@ import { testBehaviour } from "./test-context.ts"; describe("network-bans", () => { describe("network-bans:get", () => { testBehaviour("renders fixture data in output", async ({ run, projectRef }) => { - const result = await run([ - "network-bans", - "get", - "--experimental", - "--project-ref", - projectRef, - ]); + const result = await run(["network-bans", "get", "--experimental", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); }); @@ -22,8 +16,7 @@ describe("network-bans", () => { "--experimental", "--output", "json", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -34,8 +27,7 @@ describe("network-bans", () => { "get", "--experimental", "--debug", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stderr).toMatch(/HTTP.*GET:/); @@ -51,8 +43,7 @@ describe("network-bans", () => { "network-bans", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -68,8 +59,7 @@ describe("network-bans", () => { "network-bans", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); @@ -85,8 +75,7 @@ describe("network-bans", () => { "network-bans", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -102,8 +91,7 @@ describe("network-bans", () => { "network-bans", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -119,8 +107,7 @@ describe("network-bans", () => { "network-bans", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -135,8 +122,8 @@ describe("network-bans", () => { "--experimental", "--db-unban-ip", "1.2.3.4", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -150,8 +137,8 @@ describe("network-bans", () => { "1.2.3.4", "--db-unban-ip", "5.6.7.8", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -163,8 +150,8 @@ describe("network-bans", () => { "--experimental", "--db-unban-ip", "invalid-ip", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("invalid IP address"); @@ -182,8 +169,8 @@ describe("network-bans", () => { "--experimental", "--db-unban-ip", "1.2.3.4", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -201,8 +188,8 @@ describe("network-bans", () => { "--experimental", "--db-unban-ip", "1.2.3.4", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -217,8 +204,7 @@ describe("network-restrictions", () => { "network-restrictions", "get", "--experimental", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -230,8 +216,7 @@ describe("network-restrictions", () => { "--experimental", "--output", "json", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -246,8 +231,7 @@ describe("network-restrictions", () => { "network-restrictions", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -263,8 +247,7 @@ describe("network-restrictions", () => { "network-restrictions", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); @@ -280,8 +263,7 @@ describe("network-restrictions", () => { "network-restrictions", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -297,8 +279,7 @@ describe("network-restrictions", () => { "network-restrictions", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -314,8 +295,7 @@ describe("network-restrictions", () => { "network-restrictions", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -330,8 +310,8 @@ describe("network-restrictions", () => { "--experimental", "--db-allow-cidr", "0.0.0.0/0", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -344,8 +324,8 @@ describe("network-restrictions", () => { "--append", "--db-allow-cidr", "8.8.8.0/24", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -358,8 +338,8 @@ describe("network-restrictions", () => { "--bypass-cidr-checks", "--db-allow-cidr", "0.0.0.0/0", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -371,8 +351,8 @@ describe("network-restrictions", () => { "--experimental", "--db-allow-cidr", "not-a-cidr", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("failed to parse IP"); @@ -390,8 +370,8 @@ describe("network-restrictions", () => { "--experimental", "--db-allow-cidr", "0.0.0.0/0", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -409,8 +389,8 @@ describe("network-restrictions", () => { "--experimental", "--db-allow-cidr", "0.0.0.0/0", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -428,8 +408,8 @@ describe("network-restrictions", () => { "--experimental", "--db-allow-cidr", "0.0.0.0/0", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -447,8 +427,8 @@ describe("network-restrictions", () => { "--experimental", "--db-allow-cidr", "0.0.0.0/0", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -463,8 +443,7 @@ describe("ssl-enforcement", () => { "ssl-enforcement", "get", "--experimental", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -476,8 +455,7 @@ describe("ssl-enforcement", () => { "--experimental", "--output", "json", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -492,8 +470,7 @@ describe("ssl-enforcement", () => { "ssl-enforcement", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -509,8 +486,7 @@ describe("ssl-enforcement", () => { "ssl-enforcement", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); @@ -526,8 +502,7 @@ describe("ssl-enforcement", () => { "ssl-enforcement", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -543,8 +518,7 @@ describe("ssl-enforcement", () => { "ssl-enforcement", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -560,8 +534,7 @@ describe("ssl-enforcement", () => { "ssl-enforcement", "get", "--experimental", - "--project-ref", - PROJECT_REF, + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -575,8 +548,8 @@ describe("ssl-enforcement", () => { "update", "--experimental", "--enable-db-ssl-enforcement", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -587,8 +560,8 @@ describe("ssl-enforcement", () => { "update", "--experimental", "--disable-db-ssl-enforcement", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); }); @@ -600,8 +573,8 @@ describe("ssl-enforcement", () => { "--experimental", "--enable-db-ssl-enforcement", "--disable-db-ssl-enforcement", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); }); @@ -611,8 +584,8 @@ describe("ssl-enforcement", () => { "ssl-enforcement", "update", "--experimental", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); }); @@ -628,8 +601,8 @@ describe("ssl-enforcement", () => { "update", "--experimental", "--enable-db-ssl-enforcement", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -646,8 +619,8 @@ describe("ssl-enforcement", () => { "update", "--experimental", "--enable-db-ssl-enforcement", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); @@ -664,8 +637,8 @@ describe("ssl-enforcement", () => { "update", "--experimental", "--enable-db-ssl-enforcement", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Project not found"); @@ -682,8 +655,8 @@ describe("ssl-enforcement", () => { "update", "--experimental", "--enable-db-ssl-enforcement", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -700,8 +673,8 @@ describe("ssl-enforcement", () => { "update", "--experimental", "--enable-db-ssl-enforcement", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); diff --git a/apps/cli-e2e/src/tests/orgs.e2e.test.ts b/apps/cli-e2e/src/tests/orgs.e2e.test.ts index 028ab99aed..74964819b3 100644 --- a/apps/cli-e2e/src/tests/orgs.e2e.test.ts +++ b/apps/cli-e2e/src/tests/orgs.e2e.test.ts @@ -74,14 +74,14 @@ describe("orgs", () => { describe("orgs:create", () => { testBehaviour.skipIf(isRecording)("creates organization with name", async ({ run }) => { - const result = await run(["orgs", "create", ORG_NAME]); + const result = await run(["orgs", "create", ORG_NAME, "--yes"]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("Created organization:"); expect(result.stdout).toMatch(/[a-z]{20}/); }); testBehaviour("exits non-zero without name in non-TTY", async ({ run }) => { - const result = await run(["orgs", "create"]); + const result = await run(["orgs", "create", "--yes"]); expect(result.exitCode).not.toBe(0); }); @@ -94,7 +94,7 @@ describe("orgs", () => { body: { message: "Organization name already in use" }, }), }); - const result = await run(["orgs", "create", ORG_NAME]); + const result = await run(["orgs", "create", ORG_NAME, "--yes"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Organization name already in use"); }); @@ -105,7 +105,7 @@ describe("orgs", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Organization limit reached" } }), }); - const result = await run(["orgs", "create", ORG_NAME]); + const result = await run(["orgs", "create", ORG_NAME, "--yes"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Organization limit reached"); }); @@ -116,7 +116,7 @@ describe("orgs", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 422, body: { message: "Invalid organization name" } }), }); - const result = await run(["orgs", "create", ORG_NAME]); + const result = await run(["orgs", "create", ORG_NAME, "--yes"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid organization name"); }); @@ -127,7 +127,7 @@ describe("orgs", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["orgs", "create", ORG_NAME]); + const result = await run(["orgs", "create", ORG_NAME, "--yes"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -138,7 +138,7 @@ describe("orgs", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["orgs", "create", ORG_NAME]); + const result = await run(["orgs", "create", ORG_NAME, "--yes"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); }); @@ -149,7 +149,7 @@ describe("orgs", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["orgs", "create", ORG_NAME]); + const result = await run(["orgs", "create", ORG_NAME, "--yes"]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); }); diff --git a/apps/cli-e2e/src/tests/projects.e2e.test.ts b/apps/cli-e2e/src/tests/projects.e2e.test.ts index e34dc96926..3dd0f694ae 100644 --- a/apps/cli-e2e/src/tests/projects.e2e.test.ts +++ b/apps/cli-e2e/src/tests/projects.e2e.test.ts @@ -68,7 +68,7 @@ describe("projects", () => { describe("projects:api-keys", () => { testBehaviour("shows default and anon keys", async ({ run, projectRef }) => { - const result = await run(["projects", "api-keys", "--project-ref", projectRef]); + const result = await run(["projects", "api-keys", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("NAME"); expect(result.stdout).toContain("KEY VALUE"); @@ -82,8 +82,7 @@ describe("projects", () => { "api-keys", "--output", "json", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); const parsed = JSON.parse(result.stdout) as unknown[]; @@ -97,7 +96,7 @@ describe("projects", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["projects", "api-keys", "--project-ref", PROJECT_REF]); + const result = await run(["projects", "api-keys", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); @@ -107,7 +106,7 @@ describe("projects", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["projects", "api-keys", "--project-ref", PROJECT_REF]); + const result = await run(["projects", "api-keys", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); @@ -117,7 +116,7 @@ describe("projects", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), }); - const result = await run(["projects", "api-keys", "--project-ref", PROJECT_REF]); + const result = await run(["projects", "api-keys", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); }); @@ -136,13 +135,14 @@ describe("projects", () => { "us-east-1", "--size", "micro", + "--yes", ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("REFERENCE ID"); }); testBehaviour("exits non-zero without required flags in non-TTY", async ({ run }) => { - const result = await run(["projects", "create", "--org-id", "test-org-id"]); + const result = await run(["projects", "create", "--org-id", "test-org-id", "--yes"]); expect(result.exitCode).not.toBe(0); }); @@ -162,6 +162,7 @@ describe("projects", () => { "password123", "--region", "us-east-1", + "--yes", ]); expect(result.exitCode).not.toBe(0); }); @@ -188,6 +189,7 @@ describe("projects", () => { "password123", "--region", "us-east-1", + "--yes", ]); expect(result.exitCode).not.toBe(0); }); @@ -208,6 +210,7 @@ describe("projects", () => { "password123", "--region", "us-east-1", + "--yes", ]); expect(result.exitCode).not.toBe(0); }); @@ -228,6 +231,7 @@ describe("projects", () => { "password123", "--region", "us-east-1", + "--yes", ]); expect(result.exitCode).not.toBe(0); }); @@ -248,6 +252,7 @@ describe("projects", () => { "password123", "--region", "us-east-1", + "--yes", ]); expect(result.exitCode).not.toBe(0); }); diff --git a/apps/cli-e2e/src/tests/request-log.ts b/apps/cli-e2e/src/tests/request-log.ts new file mode 100644 index 0000000000..4b62c26abb --- /dev/null +++ b/apps/cli-e2e/src/tests/request-log.ts @@ -0,0 +1,12 @@ +export interface RequestEntry { + method: string; + pathname: string; + headers: Record; + body: unknown; +} + +/** The requests the replay server received since the last test cleared its log. */ +export async function getRequestLog(apiUrl: string): Promise { + const res = await fetch(`${apiUrl}/_ctrl/requests`); + return res.json() as Promise; +} diff --git a/apps/cli-e2e/src/tests/secrets.e2e.test.ts b/apps/cli-e2e/src/tests/secrets.e2e.test.ts index 9ace9e4e12..a3d2924351 100644 --- a/apps/cli-e2e/src/tests/secrets.e2e.test.ts +++ b/apps/cli-e2e/src/tests/secrets.e2e.test.ts @@ -7,7 +7,7 @@ import { testBehaviour } from "./test-context.ts"; describe("secrets", () => { describe("secrets:list", () => { testBehaviour("renders fixture data in output", async ({ run, projectRef }) => { - const result = await run(["secrets", "list", "--project-ref", projectRef]); + const result = await run(["secrets", "list", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("NAME"); expect(result.stdout).toContain("DIGEST"); @@ -19,19 +19,12 @@ describe("secrets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["secrets", "list", "--project-ref", PROJECT_REF]); + const result = await run(["secrets", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); testBehaviour("returns json output with --output json", async ({ run, projectRef }) => { - const result = await run([ - "secrets", - "list", - "--output", - "json", - "--project-ref", - projectRef, - ]); + const result = await run(["secrets", "list", "--output", "json", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); const parsed = JSON.parse(result.stdout) as unknown[]; expect(Array.isArray(parsed)).toBe(true); @@ -43,7 +36,7 @@ describe("secrets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["secrets", "list", "--project-ref", PROJECT_REF]); + const result = await run(["secrets", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); @@ -53,7 +46,7 @@ describe("secrets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), }); - const result = await run(["secrets", "list", "--project-ref", PROJECT_REF]); + const result = await run(["secrets", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); @@ -63,7 +56,7 @@ describe("secrets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["secrets", "list", "--project-ref", PROJECT_REF]); + const result = await run(["secrets", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); @@ -73,19 +66,19 @@ describe("secrets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["secrets", "list", "--project-ref", PROJECT_REF]); + const result = await run(["secrets", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); testBehaviour("includes debug output with --debug", async ({ run, projectRef }) => { - const result = await run(["secrets", "list", "--debug", "--project-ref", projectRef]); + const result = await run(["secrets", "list", "--debug", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); }); }); describe("secrets:set", () => { testBehaviour("sets a single secret", async ({ run, projectRef }) => { - const result = await run(["secrets", "set", "FOO=bar", "--project-ref", projectRef]); + const result = await run(["secrets", "set", "FOO=bar", "--yes", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("Finished"); }); @@ -96,8 +89,8 @@ describe("secrets", () => { "set", "FOO=bar", "BAZ=qux", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("Finished"); @@ -110,8 +103,8 @@ describe("secrets", () => { "set", "--env-file", ".env.local", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("Finished"); @@ -126,7 +119,7 @@ describe("secrets", () => { body: { message: "Validation failed", errors: [{ message: "Invalid secret name" }] }, }), }); - const result = await run(["secrets", "set", "FOO=bar", "--project-ref", PROJECT_REF]); + const result = await run(["secrets", "set", "FOO=bar", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); @@ -136,8 +129,8 @@ describe("secrets", () => { "set", "--env-file", "nonexistent.env", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); }); @@ -148,28 +141,27 @@ describe("secrets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["secrets", "set", "FOO=bar", "--project-ref", PROJECT_REF]); + const result = await run(["secrets", "set", "FOO=bar", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); }); describe("secrets:unset", () => { testBehaviour("removes a secret", async ({ run, projectRef }) => { - const result = await run(["secrets", "unset", "FOO", "--project-ref", projectRef, "--yes"]); + const result = await run(["secrets", "unset", "FOO", "--yes", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("Finished"); }); testBehaviour("removes multiple secrets", async ({ run, projectRef }) => { - await run(["secrets", "set", "FOO=bar", "BAR=baz", "--project-ref", projectRef]); + await run(["secrets", "set", "FOO=bar", "BAR=baz", "--yes", `--remote=${projectRef}`]); const result = await run([ "secrets", "unset", "FOO", "BAR", - "--project-ref", - projectRef, "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("Finished"); @@ -185,9 +177,8 @@ describe("secrets", () => { "secrets", "unset", "NONEXISTENT", - "--project-ref", - PROJECT_REF, "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); }); @@ -198,7 +189,7 @@ describe("secrets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["secrets", "unset", "FOO", "--project-ref", PROJECT_REF, "--yes"]); + const result = await run(["secrets", "unset", "FOO", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); @@ -208,7 +199,7 @@ describe("secrets", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["secrets", "unset", "FOO", "--project-ref", PROJECT_REF, "--yes"]); + const result = await run(["secrets", "unset", "FOO", "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); }); }); diff --git a/apps/cli-e2e/src/tests/sso.e2e.test.ts b/apps/cli-e2e/src/tests/sso.e2e.test.ts index f717699f92..af6a369fc4 100644 --- a/apps/cli-e2e/src/tests/sso.e2e.test.ts +++ b/apps/cli-e2e/src/tests/sso.e2e.test.ts @@ -14,19 +14,19 @@ const MINIMAL_SAML_XML = ` describe("sso", () => { describe("sso:list", () => { testBehaviour("renders fixture data in output", async ({ run, projectRef }) => { - const result = await run(["sso", "list", "--project-ref", projectRef]); + const result = await run(["sso", "list", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("IDENTITY PROVIDER ID"); }); testBehaviour("returns json output with --output json", async ({ run, projectRef }) => { - const result = await run(["sso", "list", "--output", "json", "--project-ref", projectRef]); + const result = await run(["sso", "list", "--output", "json", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(JSON.parse(result.stdout)).toMatchObject({ providers: [] }); }); testBehaviour("includes debug output with --debug", async ({ run, projectRef }) => { - const result = await run(["sso", "list", "--debug", "--project-ref", projectRef]); + const result = await run(["sso", "list", "--debug", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stderr).toMatch(/HTTP.*GET:/); }); @@ -37,7 +37,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["sso", "list", "--project-ref", PROJECT_REF]); + const result = await run(["sso", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -48,7 +48,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["sso", "list", "--project-ref", PROJECT_REF]); + const result = await run(["sso", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); }); @@ -59,7 +59,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), }); - const result = await run(["sso", "list", "--project-ref", PROJECT_REF]); + const result = await run(["sso", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("SAML 2.0 support is not enabled"); }); @@ -70,7 +70,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["sso", "list", "--project-ref", PROJECT_REF]); + const result = await run(["sso", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); }); @@ -81,7 +81,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["sso", "list", "--project-ref", PROJECT_REF]); + const result = await run(["sso", "list", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); }); @@ -89,13 +89,13 @@ describe("sso", () => { describe("sso:info", () => { testBehaviour("renders fixture data in output", async ({ run, projectRef }) => { - const result = await run(["sso", "info", "--project-ref", projectRef]); + const result = await run(["sso", "info", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("supabase.co/auth/v1/sso/saml/acs"); }); testBehaviour("returns json output with --output json", async ({ run, projectRef }) => { - const result = await run(["sso", "info", "--output", "json", "--project-ref", projectRef]); + const result = await run(["sso", "info", "--output", "json", `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(JSON.parse(result.stdout)).toMatchObject({ acs_url: expect.stringContaining("supabase.co/auth/v1/sso/saml/acs"), @@ -109,7 +109,7 @@ describe("sso", () => { testBehaviour.skipIf(isRecording)( "renders fixture data in output", async ({ run, projectRef }) => { - const result = await run(["sso", "show", PROVIDER_ID, "--project-ref", projectRef]); + const result = await run(["sso", "show", PROVIDER_ID, `--remote=${projectRef}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("example.com"); }, @@ -124,8 +124,7 @@ describe("sso", () => { PROVIDER_ID, "--output", "json", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("example.com"); @@ -140,8 +139,7 @@ describe("sso", () => { "show", PROVIDER_ID, "--metadata", - "--project-ref", - projectRef, + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("EntityDescriptor"); @@ -154,7 +152,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["sso", "show", PROVIDER_ID, "--project-ref", PROJECT_REF]); + const result = await run(["sso", "show", PROVIDER_ID, `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -165,7 +163,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["sso", "show", PROVIDER_ID, "--project-ref", PROJECT_REF]); + const result = await run(["sso", "show", PROVIDER_ID, `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); }); @@ -176,7 +174,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "SSO Identity Provider not found" } }), }); - const result = await run(["sso", "show", PROVIDER_ID, "--project-ref", PROJECT_REF]); + const result = await run(["sso", "show", PROVIDER_ID, `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("could not be found"); }); @@ -187,7 +185,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["sso", "show", PROVIDER_ID, "--project-ref", PROJECT_REF]); + const result = await run(["sso", "show", PROVIDER_ID, `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); }); @@ -198,7 +196,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["sso", "show", PROVIDER_ID, "--project-ref", PROJECT_REF]); + const result = await run(["sso", "show", PROVIDER_ID, `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); }); @@ -219,8 +217,8 @@ describe("sso", () => { metadataPath, "--domains", "example.com", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("example.com"); @@ -234,8 +232,8 @@ describe("sso", () => { "--metadata-url", "https://example.com/saml/metadata", "--skip-url-validation", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain('"type"'); @@ -255,8 +253,8 @@ describe("sso", () => { "https://example.com/saml/metadata", "--metadata-file", metadataPath, - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("metadata"); @@ -271,8 +269,8 @@ describe("sso", () => { "saml", "--metadata-url", "http://localhost:19999/saml.xml", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("HTTPS"); @@ -293,8 +291,8 @@ describe("sso", () => { "saml", "--metadata-file", metadataPath, - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -315,8 +313,8 @@ describe("sso", () => { "saml", "--metadata-file", metadataPath, - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); @@ -340,8 +338,8 @@ describe("sso", () => { "saml", "--metadata-file", metadataPath, - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid SAML metadata"); @@ -362,8 +360,8 @@ describe("sso", () => { "saml", "--metadata-file", metadataPath, - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -384,8 +382,8 @@ describe("sso", () => { "saml", "--metadata-file", metadataPath, - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -402,8 +400,8 @@ describe("sso", () => { PROVIDER_ID, "--add-domains", "example.com", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("example.com"); @@ -419,8 +417,8 @@ describe("sso", () => { PROVIDER_ID, "--domains", "new.com", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("new.com"); @@ -436,8 +434,8 @@ describe("sso", () => { PROVIDER_ID, "--remove-domains", "example.com", - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("IDENTITY PROVIDER ID"); @@ -455,8 +453,8 @@ describe("sso", () => { PROVIDER_ID, "--metadata-file", metadataPath, - "--project-ref", - projectRef, + "--yes", + `--remote=${projectRef}`, ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("EntityDescriptor"); @@ -472,8 +470,8 @@ describe("sso", () => { "a.com", "--add-domains", "b.com", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("domains"); @@ -491,8 +489,8 @@ describe("sso", () => { PROVIDER_ID, "--add-domains", "example.com", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); @@ -510,8 +508,8 @@ describe("sso", () => { PROVIDER_ID, "--add-domains", "example.com", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); @@ -529,8 +527,8 @@ describe("sso", () => { PROVIDER_ID, "--add-domains", "example.com", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("could not be found"); @@ -548,8 +546,8 @@ describe("sso", () => { PROVIDER_ID, "--add-domains", "example.com", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); @@ -567,8 +565,8 @@ describe("sso", () => { PROVIDER_ID, "--add-domains", "example.com", - "--project-ref", - PROJECT_REF, + "--yes", + `--remote=${PROJECT_REF}`, ]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); @@ -577,7 +575,7 @@ describe("sso", () => { describe("sso:remove", () => { testBehaviour.skipIf(isRecording)("removes a provider", async ({ run }) => { - const result = await run(["sso", "remove", PROVIDER_ID, "--project-ref", PROJECT_REF]); + const result = await run(["sso", "remove", PROVIDER_ID, "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("example.com"); }); @@ -588,7 +586,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), }); - const result = await run(["sso", "remove", PROVIDER_ID, "--project-ref", PROJECT_REF]); + const result = await run(["sso", "remove", PROVIDER_ID, "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Invalid token"); }); @@ -599,7 +597,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), }); - const result = await run(["sso", "remove", PROVIDER_ID, "--project-ref", PROJECT_REF]); + const result = await run(["sso", "remove", PROVIDER_ID, "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Forbidden"); }); @@ -610,7 +608,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 404, body: { message: "SSO Identity Provider not found" } }), }); - const result = await run(["sso", "remove", PROVIDER_ID, "--project-ref", PROJECT_REF]); + const result = await run(["sso", "remove", PROVIDER_ID, "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("could not be found"); }); @@ -621,7 +619,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), }); - const result = await run(["sso", "remove", PROVIDER_ID, "--project-ref", PROJECT_REF]); + const result = await run(["sso", "remove", PROVIDER_ID, "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Too Many Requests"); }); @@ -632,7 +630,7 @@ describe("sso", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), }); - const result = await run(["sso", "remove", PROVIDER_ID, "--project-ref", PROJECT_REF]); + const result = await run(["sso", "remove", PROVIDER_ID, "--yes", `--remote=${PROJECT_REF}`]); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("Internal Server Error"); }); diff --git a/apps/cli-e2e/src/tests/stack.e2e.test.ts b/apps/cli-e2e/src/tests/stack.e2e.test.ts index 8993de60eb..d36ea2ae23 100644 --- a/apps/cli-e2e/src/tests/stack.e2e.test.ts +++ b/apps/cli-e2e/src/tests/stack.e2e.test.ts @@ -2,6 +2,7 @@ import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, inject, test } from "vitest"; import { createHarness, exec, type CLIResult } from "@supabase/cli-test-helpers"; +import { getRequestLog } from "./request-log.ts"; import { testBehaviour } from "./test-context.ts"; import { ACCESS_TOKEN } from "./env.ts"; @@ -122,9 +123,7 @@ describe("seed buckets", () => { ); const result = await run(["seed", "buckets"]); expect(result.exitCode).toBe(0); - const requests = await fetch(`${apiUrl}/_ctrl/requests`).then( - (r) => r.json() as Promise>, - ); + const requests = await getRequestLog(apiUrl); expect(requests.some((r) => r.method === "POST" && r.pathname === "/storage/v1/bucket")).toBe( true, ); diff --git a/apps/cli-e2e/src/tests/storage.e2e.test.ts b/apps/cli-e2e/src/tests/storage.e2e.test.ts index b0975ba22d..21f1144fb8 100644 --- a/apps/cli-e2e/src/tests/storage.e2e.test.ts +++ b/apps/cli-e2e/src/tests/storage.e2e.test.ts @@ -1,10 +1,11 @@ import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect } from "vitest"; +import { getRequestLog } from "./request-log.ts"; import { testBehaviour } from "./test-context.ts"; const BUCKET = "cli-e2e-bucket"; -const LOCAL_FLAGS = ["--experimental", "--local"]; +const EXPERIMENTAL_FLAGS = ["--experimental"]; function setupStorageWorkspace(dir: string, relayUrl: string): void { mkdirSync(join(dir, "supabase"), { recursive: true }); @@ -15,18 +16,6 @@ function setupStorageWorkspace(dir: string, relayUrl: string): void { writeFileSync(join(dir, "upload.txt"), "test upload content"); } -interface RequestEntry { - method: string; - pathname: string; - headers: Record; - body: unknown; -} - -async function getRequestLog(apiUrl: string): Promise { - const res = await fetch(`${apiUrl}/_ctrl/requests`); - return res.json() as Promise; -} - async function injectGlobalError(apiUrl: string, status: number, message: string): Promise { await fetch(`${apiUrl}/_ctrl/error-all`, { method: "POST", @@ -42,7 +31,7 @@ async function clearOverrides(apiUrl: string): Promise { describe("storage ls", () => { testBehaviour("lists objects in bucket", async ({ workspace, run, apiUrl }) => { setupStorageWorkspace(workspace.path, apiUrl); - const result = await run(["storage", "ls", ...LOCAL_FLAGS, `ss:///${BUCKET}/`]); + const result = await run(["storage", "ls", ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/`]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("hello.txt"); const requests = await getRequestLog(apiUrl); @@ -55,7 +44,7 @@ describe("storage ls", () => { testBehaviour("lists objects recursively", async ({ workspace, run, apiUrl }) => { setupStorageWorkspace(workspace.path, apiUrl); - const result = await run(["storage", "ls", ...LOCAL_FLAGS, "-r", `ss:///${BUCKET}/`]); + const result = await run(["storage", "ls", ...EXPERIMENTAL_FLAGS, "-r", `ss:///${BUCKET}/`]); expect(result.exitCode).toBe(0); const requests = await getRequestLog(apiUrl); expect( @@ -68,7 +57,7 @@ describe("storage ls", () => { testBehaviour("exits 1 on 401", async ({ workspace, run, apiUrl }) => { setupStorageWorkspace(workspace.path, apiUrl); await injectGlobalError(apiUrl, 401, "Invalid token"); - const result = await run(["storage", "ls", ...LOCAL_FLAGS, `ss:///${BUCKET}/`]); + const result = await run(["storage", "ls", ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/`]); expect(result.exitCode).toBe(1); expect(result.stderr).toContain("Invalid token"); await clearOverrides(apiUrl); @@ -77,7 +66,7 @@ describe("storage ls", () => { testBehaviour("exits 1 on 403", async ({ workspace, run, apiUrl }) => { setupStorageWorkspace(workspace.path, apiUrl); await injectGlobalError(apiUrl, 403, "Forbidden"); - const result = await run(["storage", "ls", ...LOCAL_FLAGS, `ss:///${BUCKET}/`]); + const result = await run(["storage", "ls", ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/`]); expect(result.exitCode).toBe(1); expect(result.stderr.length).toBeGreaterThan(0); await clearOverrides(apiUrl); @@ -86,7 +75,7 @@ describe("storage ls", () => { testBehaviour("exits 1 on 429", async ({ workspace, run, apiUrl }) => { setupStorageWorkspace(workspace.path, apiUrl); await injectGlobalError(apiUrl, 429, "Too Many Requests"); - const result = await run(["storage", "ls", ...LOCAL_FLAGS, `ss:///${BUCKET}/`]); + const result = await run(["storage", "ls", ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/`]); expect(result.exitCode).toBe(1); expect(result.stderr.length).toBeGreaterThan(0); await clearOverrides(apiUrl); @@ -95,7 +84,7 @@ describe("storage ls", () => { testBehaviour("exits 1 on 500", async ({ workspace, run, apiUrl }) => { setupStorageWorkspace(workspace.path, apiUrl); await injectGlobalError(apiUrl, 500, "Internal Server Error"); - const result = await run(["storage", "ls", ...LOCAL_FLAGS, `ss:///${BUCKET}/`]); + const result = await run(["storage", "ls", ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/`]); expect(result.exitCode).toBe(1); expect(result.stderr.length).toBeGreaterThan(0); await clearOverrides(apiUrl); @@ -108,7 +97,7 @@ describe("storage cp", () => { const result = await run([ "storage", "cp", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, "upload.txt", `ss:///${BUCKET}/upload.txt`, ]); @@ -126,7 +115,7 @@ describe("storage cp", () => { const result = await run([ "storage", "cp", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, "--cache-control", "no-cache", "upload.txt", @@ -146,7 +135,7 @@ describe("storage cp", () => { const result = await run([ "storage", "cp", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, "--content-type", "application/json", "upload.txt", @@ -166,7 +155,7 @@ describe("storage cp", () => { const result = await run([ "storage", "cp", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, "nonexistent.txt", `ss:///${BUCKET}/x.txt`, ]); @@ -179,7 +168,7 @@ describe("storage cp", () => { const result = await run([ "storage", "cp", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, "upload.txt", `ss:///${BUCKET}/upload.txt`, ]); @@ -194,7 +183,7 @@ describe("storage cp", () => { const result = await run([ "storage", "cp", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, "upload.txt", `ss:///${BUCKET}/upload.txt`, ]); @@ -209,7 +198,7 @@ describe("storage cp", () => { const result = await run([ "storage", "cp", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, "upload.txt", `ss:///${BUCKET}/upload.txt`, ]); @@ -224,7 +213,7 @@ describe("storage cp", () => { const result = await run([ "storage", "cp", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, "upload.txt", `ss:///${BUCKET}/upload.txt`, ]); @@ -238,7 +227,7 @@ describe("storage cp", () => { const result = await run([ "storage", "cp", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/hello.txt`, "hello-download.txt", ]); @@ -250,11 +239,17 @@ describe("storage mv", () => { testBehaviour("moves file within bucket", async ({ workspace, run, apiUrl }) => { setupStorageWorkspace(workspace.path, apiUrl); // Upload source file so the move has something to move in staging. - await run(["storage", "cp", ...LOCAL_FLAGS, "upload.txt", `ss:///${BUCKET}/mv-source.txt`]); + await run([ + "storage", + "cp", + ...EXPERIMENTAL_FLAGS, + "upload.txt", + `ss:///${BUCKET}/mv-source.txt`, + ]); const result = await run([ "storage", "mv", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/mv-source.txt`, `ss:///${BUCKET}/mv-dest.txt`, ]); @@ -277,7 +272,7 @@ describe("storage mv", () => { const result = await run([ "storage", "mv", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/a.txt`, `ss:///${BUCKET}/b.txt`, ]); @@ -292,7 +287,7 @@ describe("storage mv", () => { const result = await run([ "storage", "mv", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/a.txt`, `ss:///${BUCKET}/b.txt`, ]); @@ -307,7 +302,7 @@ describe("storage mv", () => { const result = await run([ "storage", "mv", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/a.txt`, `ss:///${BUCKET}/b.txt`, ]); @@ -322,7 +317,7 @@ describe("storage mv", () => { const result = await run([ "storage", "mv", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/a.txt`, `ss:///${BUCKET}/b.txt`, ]); @@ -336,12 +331,18 @@ describe("storage rm", () => { testBehaviour("removes a file from storage", async ({ workspace, run, apiUrl }) => { setupStorageWorkspace(workspace.path, apiUrl); // Upload the file to remove. - await run(["storage", "cp", ...LOCAL_FLAGS, "upload.txt", `ss:///${BUCKET}/rm-target.txt`]); + await run([ + "storage", + "cp", + ...EXPERIMENTAL_FLAGS, + "upload.txt", + `ss:///${BUCKET}/rm-target.txt`, + ]); const result = await run([ "storage", "rm", "--yes", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/rm-target.txt`, ]); expect(result.exitCode).toBe(0); @@ -356,13 +357,13 @@ describe("storage rm", () => { testBehaviour("removes multiple files", async ({ workspace, run, apiUrl }) => { setupStorageWorkspace(workspace.path, apiUrl); writeFileSync(join(workspace.path, "file2.txt"), "second file"); - await run(["storage", "cp", ...LOCAL_FLAGS, "upload.txt", `ss:///${BUCKET}/rm-a.txt`]); - await run(["storage", "cp", ...LOCAL_FLAGS, "file2.txt", `ss:///${BUCKET}/rm-b.txt`]); + await run(["storage", "cp", ...EXPERIMENTAL_FLAGS, "upload.txt", `ss:///${BUCKET}/rm-a.txt`]); + await run(["storage", "cp", ...EXPERIMENTAL_FLAGS, "file2.txt", `ss:///${BUCKET}/rm-b.txt`]); const result = await run([ "storage", "rm", "--yes", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/rm-a.txt`, `ss:///${BUCKET}/rm-b.txt`, ]); @@ -382,7 +383,7 @@ describe("storage rm", () => { "storage", "rm", "--yes", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/file.txt`, ]); expect(result.exitCode).toBe(1); @@ -397,7 +398,7 @@ describe("storage rm", () => { "storage", "rm", "--yes", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/file.txt`, ]); expect(result.exitCode).toBe(1); @@ -412,7 +413,7 @@ describe("storage rm", () => { "storage", "rm", "--yes", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/file.txt`, ]); expect(result.exitCode).toBe(1); @@ -427,7 +428,7 @@ describe("storage rm", () => { "storage", "rm", "--yes", - ...LOCAL_FLAGS, + ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/file.txt`, ]); expect(result.exitCode).toBe(1); diff --git a/apps/cli-e2e/src/tests/target-selection.e2e.test.ts b/apps/cli-e2e/src/tests/target-selection.e2e.test.ts new file mode 100644 index 0000000000..183a5f691d --- /dev/null +++ b/apps/cli-e2e/src/tests/target-selection.e2e.test.ts @@ -0,0 +1,29 @@ +import { describe, expect } from "vitest"; +import { linkProject } from "./link-project.ts"; +import { getRequestLog } from "./request-log.ts"; +import { testBehaviour } from "./test-context.ts"; + +const DEAD_DB_URL = "postgresql://postgres:postgres@localhost:1/postgres"; + +describe("remote target selection", () => { + testBehaviour( + "reads a space-separated --remote as --remote=", + async ({ run, projectRef, workspace, apiUrl }) => { + linkProject(workspace.path, projectRef); + const result = await run(["db", "advisors", "--type", "security", "--remote", projectRef]); + expect(result.exitCode).toBe(0); + const requests = await getRequestLog(apiUrl); + expect(requests.map((r) => `${r.method} ${r.pathname}`)).toContain( + `GET /v1/projects/${projectRef}/advisors/security`, + ); + }, + ); + + testBehaviour("rejects --remote= on a hosted command", async ({ run, apiUrl }) => { + const result = await run(["secrets", "list", `--remote=${DEAD_DB_URL}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("--remote takes a project ref or a branch name."); + const requests = await getRequestLog(apiUrl); + expect(requests.filter((r) => r.pathname.endsWith("/secrets"))).toEqual([]); + }); +}); diff --git a/apps/cli/AGENTS.md b/apps/cli/AGENTS.md index d791f8fdb3..57d76de73b 100644 --- a/apps/cli/AGENTS.md +++ b/apps/cli/AGENTS.md @@ -186,10 +186,14 @@ property shape (`flags`, `is_agent`, and `env_signals`), use canonical names and [event catalog](src/shared/telemetry/event-catalog.ts), and redact sensitive values. `safeFlags` may include only values that carry no user data. Keep the established safe project, -project-id, org-id, and version cases. When `--project-ref` also accepts branch names, whitelist -only values matching `PROJECT_REF_PATTERN`; never log a user-created branch name. Global boolean -and choice flags are resolved by the wrapper from `globalFlagValues`; a command-local flag wins, -so a local path-valued `--output` remains redacted. Keep free-form user content redacted. +project-id, org-id, and version cases. For every command with a `hosted` or `selectable` target +policy, `withCommandTelemetry` reads the target flags from the invoked arguments, so they need no +entry in a command's `flags` or `safeFlags`: `--remote` (`true` when bare or linked, the project +ref when it is one, otherwise redacted), `--linked` and `--local` (their boolean), +`--project-ref`/`--project-id` (the value only when it matches `PROJECT_REF_PATTERN`, since it can +be a user-created branch name) and `--db-url` (always redacted). Global boolean and choice flags +are resolved by the wrapper from `globalFlagValues`; a command-local flag wins, so a local +path-valued `--output` remains redacted. Keep free-form user content redacted. Do not remove, rename, or reshape custom events. Preserve login aliasing after token persistence, link project and organization identity after the link write (including branch-link metadata), @@ -225,6 +229,31 @@ text layer. Therefore stdout is payload-only for either machine flag. Do not cha layer to move spinners; that would change text output globally. Handlers check `--output` first and use established encoders before the `output.format` branch. +## Targets and hosted consent + +Every leaf declares its target group and consent default with `withTargetPolicy`; the +classification test in `src/command-internal/target/target-policy.unit.test.ts` fails a leaf +without one. Declare the target flags only through `targetFlags` in +`src/command-internal/target/target-flags.ts`. + +- Database, storage, and seed commands call `resolveTarget`, then `resolveConnection`. Never pass a + default target: the policy owns the default, so a call site cannot fall back to local or linked. +- A command that binds `--password` passes `hasPasswordFlag: true` to `resolveConnection`. +- The linked `Target:` banner comes from `resolveTarget` and `resolveRef`; never build a linked + `ResolvedTarget` by hand. +- Hosted writes call `requireHostedMutationConsent` (or `requireTargetMutationConsent` when the + target may be local) after the plan and before the first persistent effect, and pass the + returned `HostedMutationConsent` to anything that needs proof of it. +- Never read stdin for consent. JSON, piped, and agent runs need `--yes` or a shell + `SUPABASE_YES`; `supabase/.env` and a cwd `.env*` file do not count. Write the action as an + imperative sentence-case phrase with no trailing period (`Delete project `). +- A `selectable` leaf with `consent: "none"` declares a `consentExemptReason`. +- Every leaf with consent has a `describeHostedConsent("", …)` test from + `tests/helpers/hosted-consent.ts`. + +See [ADR 0033](../../docs/adr/0033-target-selection-and-hosted-mutation-consent.md) for the +target-default and consent rules. + ## Tests Follow the root testing philosophy and use the package scripts in `package.json`. Cover meaningful diff --git a/apps/cli/docs/analytics.md b/apps/cli/docs/analytics.md index 6f994749b4..3d8bbdd5fb 100644 --- a/apps/cli/docs/analytics.md +++ b/apps/cli/docs/analytics.md @@ -96,11 +96,11 @@ Local-runtime commands also carry properties that split error rates by runtime: - `stack_backend` (`legacy` or `stack`) on commands routed by `experimental.stack`; see [backend selection](stack-commands.md) - `stack_runtime` (`docker`, `podman`, or `native`): the runtime of the stack a stack-backend - command looked up, which for `--db-url` may be the project stack even when the URL targets + command looked up, which for `--remote=` may be the project stack even when the URL targets another database - `orioledb` (boolean) once the command resolves local database config, `true` when the project selects an OrioleDB image (`db.orioledb_version` on a 15/17 project). It describes local config - only; for linked or `--db-url` targets it says nothing about the remote database + only; for `--remote` targets it says nothing about the remote database Flag capture is intentionally conservative: `flags` records only flags changed on the command line, and redacts each value unless it is boolean, a choice flag, or listed in the command's diff --git a/apps/cli/docs/command-target.md b/apps/cli/docs/command-target.md new file mode 100644 index 0000000000..e1f502897e --- /dev/null +++ b/apps/cli/docs/command-target.md @@ -0,0 +1,149 @@ +# Command target + +Every command that acts on a database or hosted project announces the target it resolved, once, before it acts. The same identity goes to machine consumers as the JSON `target` object. + +This page is the shared reference for how a command's target is chosen, which errors the selection raises, the deprecated selector flags, how a hosted write is confirmed, and what the banner and the JSON `target` carry. [ADR 0033](../../../docs/adr/0033-target-selection-and-hosted-mutation-consent.md) records why. The `SIDE_EFFECTS.md` of every `hosted`, `selectable` and `account` command opens with a `Target:` line that links here and lists only what is specific to that command: its own calls, files, exit codes and prompt, and any selector rule that differs from this page. A `local-only` command has no `Target:` line. + +## Groups + +The `Target:` line names the command's group and its consent default: `Target: , consent default `, or `Target: , no consent` when the command never asks for hosted-mutation consent. A document that covers several subcommands gives each one's default. + +| Group | Acts on | With no `--remote` | Selector flags | +| ------------ | ------------------------------- | ------------------ | ---------------------------------------------------------------------------- | +| `selectable` | a database or the Storage API | the local stack | `--remote`; deprecated `--local`, `--linked`, `--project-ref` and `--db-url` | +| `hosted` | a hosted project | the linked project | `--remote=`; deprecated `--project-ref` | +| `account` | the account or an organisation | no project | none | +| `local-only` | the local stack or the checkout | the local stack | none | + +The consent default is the answer a confirmation starts on. It is `No` for anything that replaces or deletes live state and for arbitrary SQL, migration, seed and storage writes, and `Yes` for create-only operations (`projects create`, `orgs create`, `branches create`, `domains create` and `bootstrap`). `no consent` means the command never asks for hosted-mutation consent because it only reads or writes local files. It can still ask a local question of its own, such as `config pull` before it rewrites `config.toml` or `migration fetch` before it replaces local migrations. + +## Choosing a target + +| You pass | Target | +| ------------------- | ------------------------------------------------------------------------------- | +| nothing | The local stack on a `selectable` command; the linked project on a `hosted` one | +| `--remote` | The linked project | +| `--remote=` | That project: 20 lowercase letters, never looked up as a branch | +| `--remote=` | That database: a `postgres://` or `postgresql://` URL | +| `--remote=` | A branch of the linked project, by name or UUID | + +- **The linked project** is `SUPABASE_PROJECT_ID`, else `supabase/.temp/project-ref`. With neither, an interactive terminal (stdin and stdout are TTYs and the output is text) gets the project picker (`GET /v1/projects`), and any other run fails with `ProjectRefNotLinkedError`. A malformed ref from either source fails with `InvalidProjectRefError`. `SUPABASE_PROJECT_ID` alone never makes a `selectable` command remote: it is a ref source once `--remote` is passed, and enough for the `default; pass --remote` hint. The CLI never falls back between local and remote. +- **A branch** resolves with the lookup `supabase link` uses (`GET /v1/projects/{parent}/branches`, where `{parent}` is `SUPABASE_PROJECT_ID`, else the linked project), and the command acts on that branch's own project. With no linked project a UUID is looked up with `GET /v1/branches/{id}`, and one the API can't look up fails as ``Branch "" not found. Run `supabase branches list` to see available branches.``, never as a schema error. +- **Hosted-only commands** take `--remote=` for another project, and a bare `--remote` changes nothing for them. `link` takes its target as an argument and has no `--remote`. `branches *` commands act on a parent project, so their `--remote` takes a ref, never a branch name. +- **A database URL** applies only where the remote side is Postgres. `storage`, `seed buckets` and hosted commands need a project, so they reject `--remote=` when the flags are parsed, as an invalid `--remote` value (`--remote takes a project ref or a branch name.`, or `--remote takes a project ref.` on `branches *`). The run exits 1 before anything else happens, and the URL is never echoed. A value with `://` that is not a `postgres://` or `postgresql://` URL, and a keyword connection string (`host=… user=…`), is rejected on every command and is never looked up as a branch. +- **`--remote=true|yes|on|1|y`** means a bare `--remote`. `--no-remote` and `--remote=false|no|off|0|n` fail with `--remote doesn't take false. Leave it out to .`, and an empty `--remote=` with `--remote= has an empty value. Pass --remote for the linked project or --remote=; leave it out to .` `` is `target the local database`, `target the local Storage API` or, on a hosted command, `use the linked project`; `` is `` where the command takes a database URL, `` elsewhere and `` on `branches *`. Neither error prints the command's help. +- **A local stack that is down in a linked checkout.** A database command that defaults to the local stack and can't reach it adds `To use the linked project instead, pass --remote.` to its error's suggestion when the checkout is linked (`SUPABASE_PROJECT_ID` is set, or `supabase/.temp/project-ref` names a project). `gen types` adds it to its errors for a local database container that is missing or can't be inspected. + +## `--remote ` + +`--remote=` always parses as a value. A bare `--remote` followed by a word is rewritten to `--remote=` when the command takes no operands (a hidden one does not count), when the word is a database URL, or when the command already has more words than operands. + +The one ambiguous shape is a bare `--remote` directly followed by a word the command took as an operand. It is settled before anything runs, and the error offers both readings: + +- **A project-ref-shaped word** (20 lowercase letters) fails when the command still has room for operands, with no network call: pass `--remote=` for that project, or put `--remote` last to keep the word as an operand. It is read as `--remote=` only when the command takes no operands or its operands are already full. +- **Any other word** is checked against the branches of the linked project, when one is known. The run fails when the word names a branch, equals a branch name ignoring case, or is one edit from the name of a non-default branch of five or more characters; swapping two adjacent letters is one edit. The error offers `--remote=` for the branch, or the command's operands followed by `--remote` for the linked project. Otherwise the word runs as the operand. `--remote=` is separate: its "Did you mean" suggestion allows up to two edits. +- **A listing that fails** still fails the run before anything runs, and the error gives the cause: `HTTP `, the network error, or a missing or invalid access token. It never echoes the word, and its linked-project example reads `supabase --remote`. The suggestion follows the cause: a `401` says to log in again, or to fix `SUPABASE_ACCESS_TOKEN` when the token came from the environment; a `403` says to use a token that can read the project's branches; a `404` says the linked ref may be a preview branch, so link its parent project. +- **`branches *` commands** apply only the project-ref-shaped check: their `--remote` never names a branch, so any other word is the operand and nothing more is checked. + +## Selector errors + +These fail with exit 1 before the command does any work: + +| Condition | Error | +| --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | +| more than one of `--remote`, `--linked`, `--db-url` and `--local`, or `--project-ref` with `--project-id` | `TargetSelectorConflictError` | +| `--remote` given more than once with different values | `TargetSelectorConflictError` | +| `--project-ref` or `--project-id` with `--local`, `--db-url` or `--remote=` | `TargetProjectRefConflictError` | +| an empty `--db-url`, `--project-ref` or `--project-id` | `TargetEmptyValueError` | +| `--project-ref` or `--project-id` given a URL or connection string | `TargetInvalidValueError` | +| `--remote=` on a command with no database connection | a flag-parse error: an invalid `--remote` value | +| a branch name given to a `branches *` command | `TargetNeedsProjectError` | +| a bare `--remote` followed by a project-ref-shaped word, or by a word that may name a branch | `TargetRemoteAmbiguousError` | +| `--password` with `--remote=` or the local default | `TargetPasswordConflictError` | +| `--remote=` naming no branch of the linked project | `LinkBranchNotFoundError` | +| a branch name with no linked project to search | `LinkBranchNotLinkedError` | +| linked state whose ref is malformed, so there is no project to search | `LinkParentRefInvalidError` | +| a branch that has no project ref yet because it is still provisioning | `LinkBranchNotReadyError` | +| the branch lookup fails to reach the API or gets a non-200 status | `LinkBranchListNetworkError`, `LinkBranchListStatusError` | + +A selector conflict lists only the selector flags the command has: `Only one of --remote, --linked, --db-url and --local can be set; got …`, with the suggestion to use `--remote` or `--remote=` on its own. A bare `--remote` together with `--project-ref=` is accepted: the alias's value wins and its warning prints. A `false` switch counts as absent, so `--linked=false` selects nothing. A repeated `--remote` is one selection when every occurrence resolves to the same target, and a bare `--remote` is the linked project, so `--remote --remote` and `--remote= --remote=` run. When the occurrences differ the run fails before anything runs, with `--remote is given more than once with different values; pass it once.` and the suggestion to pass `--remote` once. + +`LinkBranchNotFoundError` names the parent project, lists the branches it has, offers the nearest name as "Did you mean" and, for a word of lowercase letters, or a 20-letter word with an uppercase letter (which is still looked up as a branch and gets the lowercase spelling as a suggestion), notes that a project ref is exactly 20 lowercase letters. A `404` from the branch listing adds that the linked ref may itself be a preview branch and says to link its parent project. + +## `--password` + +`db dump`, `db pull`, `db push`, `db schema declarative generate`, `migration list`, `migration repair` and `migration squash` take `--password`, which applies only when the target is a project: a bare `--remote`, a ref or a branch. Anything else fails before the command runs, with `TargetPasswordConflictError`: + +- `--remote=`: `--password can't be used with --remote=. Put the password in the connection string: postgres://USER:PASSWORD@HOST:PORT/postgres` +- the local default: `--password only applies to a linked project. The local database uses [db].password from supabase/config.toml.`, followed by `To use the linked project instead, pass --remote.` in a linked checkout. + +A command's own `SIDE_EFFECTS.md` records where else its password can come from and when it is withheld from a target. + +## Deprecated selector flags + +`--linked`, `--project-ref`, `--db-url` and `--local` (and `gen types --project-id`) still work in v3 and are removed in v4, as is the `linked` value of `db diff --from` and `--to`, which `remote` replaces. Each goes through the same resolver as the `--remote` form it maps to and prints a warning on stderr in every output format, before the `Target:` line. The warning names only the replacement: + +| Flag | The warning says | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--linked` | `--linked is deprecated and will be removed in v4. Use --remote instead` | +| `--project-ref=` | `--project-ref is deprecated and will be removed in v4. Use --remote= instead`, or `Use --remote= instead` when the value is a branch name | +| `--db-url=` | `--db-url is deprecated and will be removed in v4. Use --remote= instead`, then `, or --remote= with an access token set` when the URL names a Supabase project | +| `--local` | `--local isn't needed (local is the default) and will be removed in v4` | + +A warning never repeats an operand, a flag value or a database URL, because a CI log would keep them, and it carries no example command. Two values have their own wording: a branch whose name is a boolean literal (`true`, `false`, `yes`, `no`, `on`, `off`, `y`, `n`, `1`, `0`) is reached with `--remote=`, and a keyword `--db-url` connection string is converted to a `postgresql://` URL for `--remote=`. + +`db diff --from`/`--to` accepts the same flags and warns the same way, but names the `--from`/`--to` value that replaces each: `Use --from=remote or --to=remote instead` for `--linked`, `Use --from= or --to= instead` for `--project-ref` (`` for a branch name) and `Use --from= or --to= instead` for `--db-url`. `--project-ref` also points a `remote` side, or the deprecated `linked`, at the project it names, and fails when neither side is `remote`; `--linked`, `--db-url` and `--local` change nothing. `--remote` stays an error there. + +## Hosted-mutation consent + +Every persistent mutation of a hosted resource asks for consent first, including a write through a non-local `--remote=`. A loopback host on the local or shadow database ports, or the local stack's endpoint, counts as local. + +- **Approval.** `--yes`, or a `SUPABASE_YES` in the shell environment. A `SUPABASE_YES` in a project `.env*` file does not count. The last `--yes` on the command line wins, and `SUPABASE_YES` is read only when `--yes` never appears, so `--yes=false --yes` confirms and `--yes --yes=false` declines. A bare `--yes` followed by `false`, `no`, `off`, `0` or `n` reads as false, as the flag parser does. Stdin is never read, so a piped `y` does nothing. The confirmation line names which one approved: `Confirmed by --yes: on `. +- **Prompt.** Only text output on an interactive stdout with a TTY stdin, with no agent driving, is asked. The question starts on the command's consent default. +- **Refusal.** JSON, stream-json, `-o json|yaml|toml|env`, agent and non-TTY runs fail with `HostedMutationConsentRequiredError` unless `--yes` is set. The error names the action and target, says why the run can't ask and tells an agent to ask the user before passing `--yes`. Nothing is changed. +- **Decline.** A declined prompt fails with `HostedMutationDeclinedError` (`context canceled`), exit 1, and nothing is changed. +- **One question.** A command asks once, after the plan is known and before the first persistent effect, and lists everything it will do. +- **Network bans.** Removing a network ban to connect needs `--yes` or a shell `SUPABASE_YES`; without one, the command fails with `LinkedDbNetworkBannedError`. Its suggestion is `Re-run with --yes to remove them, or run supabase network-bans remove --db-unban-ip --experimental --yes --remote=`, and `` is always the project whose bans blocked the connection, which after `db pull --remote=` is not the linked project. This applies to reads too. +- **Reads never prompt.** A command marked `no consent` never asks for hosted-mutation consent, on any target, and `--yes` and `SUPABASE_YES` have no effect on it. `db query --read-only` is a read and skips consent too. + +## Banner + +`Target: ` on stderr in every output format, printed once per distinct text. It is best effort: it adds no API call and no auth requirement, and it never prints credentials. + +| Target | Text | +| ------------------------------------- | ----------------------------------------------------------------------------------- | +| Local stack | `local stack (:)` | +| Local stack, nothing selected, linked | `local stack (:) · default; pass --remote for ()` | +| `--remote=` | `custom Postgres (:)`; the connection string's credentials never appear | +| Linked project the cache names | `project () · production` | +| Linked project nothing vouches for | `project `, or `project ()` when the project picker named it | +| Ref other than the linked project | `project (not the linked project)` | +| Branch found by name or UUID | `project () · branch ()` | +| The project's default branch | `project () · production` | +| Branch UUID, no linked project | `project `: the lookup does not say whether it is the default branch | + +On a `hosted` command the banner is the notice that the command is talking to a remote project, and it names the linked project the command defaults to. The `default; pass --remote` hint appears only when a project is linked, by `SUPABASE_PROJECT_ID` or the link file, and no `--remote` was passed; a bare `db push`, `db pull` or `migration fetch` in a linked checkout runs locally and prints it. + +Identity comes from `supabase/.temp/linked-project.json`, with no API call. A cache that names the resolved ref itself marks a production project. The cache can be stale or missing, so it never proves a branch: "branch" is claimed only on positive evidence, which is a branch lookup by name or UUID or a live probe. A branch's parent shows as its bare ref when the cache doesn't know its name. + +## JSON `target` + +`json` and `stream-json` results carry `target`, tagged by `kind`: + +| `kind` | Fields | +| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `local` | `host`, `port` | +| `db-url` | `host`, `port` | +| `linked` | `project_ref`, `environment` (`production`, `branch` or `unverified`), optional `project_name`, optional `parent_project_ref` (a branch's parent project) | + +`environment` is `production` when the linked-project cache names the resolved ref or the resolved branch is the project's default branch, `branch` on positive evidence of any other branch, and `unverified` otherwise, including a branch UUID looked up with no linked project. A failed identity check is `unverified`, never `branch`. + +The first announced target becomes the run's `target`, so a command that announces two (`db diff --from=remote --to=`) prints both and reports the first. `db diff --from`/`--to` announces every database endpoint in resolution order, the local stack and a database URL included, so `--from local` reports `{"kind":"local","host":"127.0.0.1","port":54322}`; `migrations` announces nothing, and the same target on both sides prints one line. It is merged into object results, the `stream-json` result event and the machine error envelope. Arrays, `--output {json,yaml,toml,env}` payloads and generated artifacts such as `gen types` output are unchanged. A command that owns extra `target` fields (for example `branch`, `local_scope`, or `config push`'s `environment`) adds them in front of the announced identity's, and its own values win on a clash. + +The schema is `CommandTargetJson` in `src/shared/output/command-target.service.ts`; every target is encoded through it before it reaches output. + +## Help and docs + +A group's help labels each subcommand `[local or remote]` (`selectable`), `[remote only]` (`hosted`) or `[local only]`; `account` and removed commands carry no label. A command's own help has a `TARGET` section above `USAGE`, and its `--remote` row reads `--remote[=]` where the command takes a database URL, `--remote[=]` where it doesn't, and `--remote[=]` on `branches *`. JSON help gives the `remote` flag `value` and `valueOptional` fields in place of the bracketed name. + +Docs and examples put `--remote` last, in the `=` form when it has a value (`db pull add_profiles --remote`, `db push --remote=`), so it cannot be read as anything else. Shell completion never offers a space-separated value for `--remote` on a command that takes operands. diff --git a/apps/cli/docs/compute-commands.md b/apps/cli/docs/compute-commands.md index f8ae017494..ae8b3a4395 100644 --- a/apps/cli/docs/compute-commands.md +++ b/apps/cli/docs/compute-commands.md @@ -47,3 +47,9 @@ settings into an ignored file. To deploy a source directory without a Compute configuration entry, `compute push` requires an explicit `--exposure public` or `--exposure private`. Record the entry in the authoritative config to persist its deployment settings. + +`compute push` and `compute delete` change the remote project, so both ask for +confirmation first, and both default to no. `compute delete` asks for the +compute's name to be typed back. Pass `--yes`, or set `SUPABASE_YES` in your +shell, to confirm in scripts and CI; without it, a run that cannot prompt fails +before changing anything. diff --git a/apps/cli/docs/stack-commands.md b/apps/cli/docs/stack-commands.md index 48b065dc6e..0fec78edc5 100644 --- a/apps/cli/docs/stack-commands.md +++ b/apps/cli/docs/stack-commands.md @@ -190,12 +190,12 @@ project config and omits the Docker-era default ports so the stack is not pinned Without the environment variable, `init` still writes the established template with those ports and without the stack flag. Blank `supabase bootstrap` uses the same scaffold. -When the flag is on, `--local` targets of the `db`, `migration`, `test db`, `gen types`, and +When the flag is on, local targets of the `db`, `migration`, `test db`, `gen types`, and `inspect` families use the project stack and provision a throwaway shadow database owned by the stack runtime. The shadow runs the project's resolved database version, the OrioleDB build when `db.orioledb_version` is set, and its baseline cache is keyed by that version. Top-level -`supabase pull` uses the same stack shadow as `db pull`. Linked and `--db-url` targets stay on the Management API for engine selection. -A `--db-url` that matches `config.toml` host and port is still rewritten like a published +`supabase pull` uses the same stack shadow as `db pull`. Targets selected with `--remote` stay on the Management API for engine selection. +A `--remote=` database URL that matches `config.toml` host and port is still rewritten like a published stack target for dump's tool container. Compose names (`supabase_db_*`, `supabase_network_*`, `db:5432`) are not used. The stack backend requires the in-process pg-delta engine; `--use-migra`, `--use-pgadmin`, `--use-pg-schema`, and `db pull --diff-engine migra` are @@ -222,34 +222,33 @@ and restores default JWT verification on the same Functions instance. Per-functi `--output-format stream-json`, readiness emits a result containing the instance ID and URL, followed by live log events. -`storage ls`/`cp`/`mv`/`rm` and `seed buckets` (including bucket seeding inside `db reset ---local`) also consult `experimental.stack`, with the same `SUPABASE_EXPERIMENTAL_STACK` +`storage ls`/`cp`/`mv`/`rm` and `seed buckets` (including bucket seeding inside `db reset`) also consult `experimental.stack`, with the same `SUPABASE_EXPERIMENTAL_STACK` env-precedence rule as `start`/`stop`/`status`. See -[Storage and bucket seeding](#storage-and-bucket-seeding) below. Explicit `--linked`/ -`--project-ref` remote targeting for these commands is unaffected by the flag either way. +[Storage and bucket seeding](#storage-and-bucket-seeding) below. Explicit `--remote` +targeting for these commands is unaffected by the flag either way. `db start` brings up a postgres-only project stack on first create. An existing stack resumes its primary database without changing other services (webhooks setup only, creating a missing `supabase_functions` schema; no second overlay or migrate-and-seed). `supabase start` while that postgres-only stack is running stops it and starts the full configured stack, keeping data. `--from-backup` is not supported on the stack path. -`db reset --local` and declarative resets rebuild the existing database while retaining stack +A local `db reset` and declarative resets rebuild the existing database while retaining stack identity, ports, and composition. See [`db/reset/SIDE_EFFECTS.md`](../src/commands/db/reset/SIDE_EFFECTS.md). -`gen types --local` and `inspect db … --local` resolve the project stack through the same -`--local` database target as `db dump`. They do not start a stack. +A local `gen types` and `inspect db …` resolve the project stack through the same +local database target as `db dump`. They do not start a stack. -On the stack backend, `db dump --local`, `migration squash`, and `test db --local` run +On the stack backend, a local `db dump`, `migration squash`, and a local `test db` run catalog tools attached to their owning stack. Tools use runtime-facing database addresses; `test db` preserves the user, password, and database selected by the CLI resolver. -External `--linked` and `--db-url` tools use CLI-owned adapters without creating a stack. +External `--remote` tools use CLI-owned adapters without creating a stack. Supported native platforms run the bundled executable with `artifact/bin` on PATH. Other platforms use a one-shot container from the catalog Postgres image. Neither path falls back to host-installed PostgreSQL tools. Missing tools fail closed; container execution requires Docker. -`db lint --local` talks to the running stack over SQL. Catalog Postgres includes +A local `db lint` talks to the running stack over SQL. Catalog Postgres includes `plpgsql_check`, so native and container stacks can `CREATE EXTENSION` inside the lint transaction (always rolled back). It does not launch a client binary. @@ -336,12 +335,12 @@ to be destroyed does not stop the rest, though the command still fails. The `experimental.stack` flag (with the usual `SUPABASE_EXPERIMENTAL_STACK=1|0` env precedence) routes several command families at once: the top-level `start`/`stop`/`status` -aliases, the `stack` namespace itself, `db`/`migration`/`test`'s `--local` targets (above), and +aliases, the `stack` namespace itself, `db`/`migration`/`test`'s local targets (above), and now `storage ls`/`cp`/`mv`/`rm` and `seed buckets` (including the bucket seeding step inside `db -reset --local`). An explicit `--linked`/`--project-ref` remote target for `storage`/`seed` is +reset`). An explicit `--remote` target for `storage`/`seed` is unaffected by the flag either way. -Under the stack backend, `--local` Storage operations resolve their endpoint and credential from +Under the stack backend, local Storage operations resolve their endpoint and credential from the selected composition rather than `[api]`/`[api.tls]`: the endpoint is the Storage member's observed HTTP binding plus `/storage/v1/...`, and the service-role JWT is generated from the primary database member's observed JWT secret. The stack is located by the project root @@ -369,7 +368,7 @@ status`/`supabase stack restart`, or `supabase start` when the stack has never b Missing Storage HTTP endpoints fail with `StackStorageCapabilityError`, as do other Storage capability failures. A stack-gateway 502/503 encountered while Storage activates is reported as `StackStorageCapabilityError` guiding the user to `supabase stack logs` then `supabase stack -restart`, not as a raw status body — but only for a `--local` target; a `--linked` failure passes +restart`, not as a raw status body — but only for a local target; a `--remote` failure passes through unchanged. All of these failures exit `1` for `storage`/`seed buckets`, and no HTTP request is sent when Storage is disabled or the stack is not running. @@ -392,6 +391,6 @@ a missing capability/endpoint, a gateway activation failure, or an invalid bucke `start` command; startup does not silently skip the configured seed. See [`stack/start/SIDE_EFFECTS.md`](../src/commands/experimental/stack/start/SIDE_EFFECTS.md). -**`db reset --local` and declarative reset** reapply the catalog, migrations, and seeds before +**A local `db reset` and declarative reset** reapply the catalog, migrations, and seeds before resuming the saved composition. Bucket-seeding failures warn after the database reset succeeds. See [`db/reset/SIDE_EFFECTS.md`](../src/commands/db/reset/SIDE_EFFECTS.md). diff --git a/apps/cli/docs/supabase/config/diff.md b/apps/cli/docs/supabase/config/diff.md index 229783224c..6f299df3ef 100644 --- a/apps/cli/docs/supabase/config/diff.md +++ b/apps/cli/docs/supabase/config/diff.md @@ -2,7 +2,7 @@ Shows the configuration differences between the local `supabase/config.toml` and the effective configuration of a remote project or branch. Read-only: it never modifies the local file or any remote configuration. -Pass `--project-ref` to compare against a specific project, or the name (or UUID) of a branch of the currently linked project — values that are exactly 20 lowercase letters are always treated as project refs. Without it, the linked project is the target. When the target ref matches a `[remotes.*]` block's `project_id`, that block's merged config is the local side of the comparison. +Pass `--remote=` to compare against a specific project, or the name (or UUID) of a branch of the currently linked project — values that are exactly 20 lowercase letters are always treated as project refs. Without it, the linked project is the target. `--project-ref` still works in v3 but is deprecated and removed in v4; use `--remote`. When the target ref matches a `[remotes.*]` block's `project_id`, that block's merged config is the local side of the comparison. Only platform-managed properties are compared. Local-stack-only sections — `[studio]`, `[analytics]`, `[functions]`, `[edge_runtime]`, port numbers, image version pins, `[db.migrations]`, and similar — have no hosted counterpart and are never reported, whether or not your file declares them. @@ -12,4 +12,4 @@ Local values are shown as the configuration your file would produce once pushed, With `--exit-code`, the command exits `2` when any difference is found, keeping exit `1` for errors — so scripts can distinguish drift from failure. -Machine-readable output is available through `--output-format json|stream-json` — a versioned payload (`schema_version`, `config_schema`, `target`, `scope`, `changes[]`, `masked[]`, `unmanaged[]`, `counts`) with per-change `path`s as segment arrays. The legacy global `-o`/`--output` flag is not supported by this command; use `--output-format json|stream-json` instead. +Machine-readable output is available through `--output-format json|stream-json` — a versioned payload (`schema_version`, `config_schema`, `target`, `scope`, `changes[]`, `masked[]`, `unmanaged[]`, `counts`) with per-change `path`s as segment arrays. `target` identifies the compared project: `kind` (`linked`), `project_ref`, `project_name` and `environment` (`production`, `branch` or `unverified`) when known, `parent_project_ref` for a branch, `branch` when it was named, and `local_scope` (`base` or `remotes.`). The legacy global `-o`/`--output` flag is not supported by this command; use `--output-format json|stream-json` instead. diff --git a/apps/cli/docs/supabase/config/pull.md b/apps/cli/docs/supabase/config/pull.md index 3e238c1d59..0b7878c0b5 100644 --- a/apps/cli/docs/supabase/config/pull.md +++ b/apps/cli/docs/supabase/config/pull.md @@ -2,9 +2,9 @@ Writes the effective configuration of a remote project or branch into the local `supabase/config.toml`/`config.json` — the write side of `supabase config diff`. Every write is a surgical, format-preserving edit: comments, key ordering, and quoting elsewhere in the file are left untouched, and only the values that actually change are rewritten. -Pass `--project-ref` to pull from a specific project, or the name (or UUID) of a branch of the currently linked project — values that are exactly 20 lowercase letters are always treated as project refs. Without it, the linked project is the target. +Pass `--remote=` to pull from a specific project, or the name (or UUID) of a branch of the currently linked project — values that are exactly 20 lowercase letters are always treated as project refs. Without it, the linked project is the target. `--project-ref` still works in v3 but is deprecated and removed in v4; use `--remote`. -Where a pulled value lands depends on whether the target is already tracked by a `[remotes.*]` block, not on how you named it: if any block's `project_id` already matches the resolved ref, that block is reused — whatever its own label — and every value lands there. Otherwise, if the target was named as a branch, a new `[remotes.]` block is created (a branch resolved by UUID falls back to the project ref itself as its label). Only when neither applies — a bare `--project-ref` naming a project directly, or the linked project with no branch involved — do values land at the config root. `--remote-label` overrides the block config pull would otherwise reuse or create; naming a block that already tracks a different project, or naming nothing while a different block already tracks this exact ref, is an error rather than a silent overwrite. A `[remotes.*]` block whose `project_id` is an `env(...)` reference that merely _resolves_ to the target ref is never reused or rewritten — that is a hard error naming the variable, since a value written there would never actually take effect on the next load. Creating a new block always writes its `project_id`, even when every value it would otherwise carry already matches the local defaults — otherwise the block could never be reused on a later pull. +Where a pulled value lands depends on whether the target is already tracked by a `[remotes.*]` block, not on how you named it: if any block's `project_id` already matches the resolved ref, that block is reused — whatever its own label — and every value lands there. Otherwise, if the target was named as a branch, a new `[remotes.]` block is created (a branch resolved by UUID falls back to the project ref itself as its label). Only when neither applies — `--remote=` naming a project directly, or the linked project (a bare `--remote` or no flag) with no branch involved — do values land at the config root. `--remote-label` overrides the block config pull would otherwise reuse or create; naming a block that already tracks a different project, or naming nothing while a different block already tracks this exact ref, is an error rather than a silent overwrite. A `[remotes.*]` block whose `project_id` is an `env(...)` reference that merely _resolves_ to the target ref is never reused or rewritten — that is a hard error naming the variable, since a value written there would never actually take effect on the next load. Creating a new block always writes its `project_id`, even when every value it would otherwise carry already matches the local defaults — otherwise the block could never be reused on a later pull. Writing to the config root can also affect `supabase start`: a handful of root-scoped settings — `auth.site_url`, `db.settings.*`, `db.major_version` (which changes what `supabase start` boots), `db.pooler.*`, and similar — also govern the local stack, so pulling a hosted value into them is a real change to local dev behavior, not just a record of the hosted project's own setting. `config pull` warns about this rather than refusing; writing the same value into a `[remotes.*]` block is unaffected. Passing `--remote-label` is the escape hatch: it diverts what would otherwise be a root-bound pull into a `[remotes.