From 22f2be719af1cb7202dcbe2b47e2cf5c9a9a6f59 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Sun, 13 Sep 2026 14:55:52 +0530 Subject: [PATCH 1/3] fix(cli): support piped config pull confirmation --- apps/cli/AGENTS.md | 5 +- .../src/commands/config/pull/pull.command.ts | 5 +- .../src/commands/config/pull/pull.e2e.test.ts | 58 ++++++++++++++++++- 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/apps/cli/AGENTS.md b/apps/cli/AGENTS.md index 647b806357..d1bd0f6acb 100644 --- a/apps/cli/AGENTS.md +++ b/apps/cli/AGENTS.md @@ -68,8 +68,9 @@ Every applicable command must preserve these invariants: [profile loader's schema](src/command-internal/profile-load.ts). Both the [E2E harness](../../packages/cli-test-helpers/src/harness.ts) and [live fixture](tests/helpers/live.ts) depend on file-path mode. -5. Sibling layers in `Layer.mergeAll` each receive required services explicitly. Production layer - changes require a CLI build and a targeted binary smoke test. +5. Sibling layers in `Layer.mergeAll` each receive required services explicitly. Any production + path that can reach `promptYesNo` provides `stdinLayer`; handler-only tests do not prove this + wiring. Production layer changes require a CLI build and a targeted binary smoke test. 6. Honor both output flags. `-o`/`--output` takes precedence over `--output-format`; a new command may reject `--output` with guidance to use `--output-format`, as established by `config diff` and `config pull`. diff --git a/apps/cli/src/commands/config/pull/pull.command.ts b/apps/cli/src/commands/config/pull/pull.command.ts index cf74207183..07f9e57903 100644 --- a/apps/cli/src/commands/config/pull/pull.command.ts +++ b/apps/cli/src/commands/config/pull/pull.command.ts @@ -1,9 +1,10 @@ -import { Option } from "effect"; +import { Layer, Option } from "effect"; import type * as CliCommand from "effect/unstable/cli/Command"; import { Command, Flag } from "effect/unstable/cli"; import { PROJECT_REF_PATTERN } from "../../../config/project-ref.service.ts"; import { withJsonErrorHandling } from "../../../shared/output/json-error-handling.ts"; +import { stdinLayer } from "../../../shared/runtime/stdin.layer.ts"; import { GLOBAL_OUTPUT_FORMATS } from "../../../command-internal/global-flags.ts"; import { managementApiRuntimeLayer } from "../../../command-internal/management-api-runtime.layer.ts"; import { withCommandTelemetry } from "../../../telemetry/command-telemetry.ts"; @@ -75,5 +76,5 @@ export const configPullCommand = Command.make("pull", config).pipe( }, ]), Command.withHandler(configPullHandler), - Command.provide(managementApiRuntimeLayer(["config", "pull"])), + Command.provide(Layer.mergeAll(managementApiRuntimeLayer(["config", "pull"]), stdinLayer)), ); diff --git a/apps/cli/src/commands/config/pull/pull.e2e.test.ts b/apps/cli/src/commands/config/pull/pull.e2e.test.ts index 9ff46b0bc1..ce8dfd6abe 100644 --- a/apps/cli/src/commands/config/pull/pull.e2e.test.ts +++ b/apps/cli/src/commands/config/pull/pull.e2e.test.ts @@ -1,13 +1,15 @@ -import { mkdtemp, rm } from "node:fs/promises"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, test } from "vitest"; import { runSupabase } from "../../../../tests/helpers/cli.ts"; +import { v2ProjectConfigResponse } from "../../../../tests/helpers/config-fixtures.ts"; // A fake-but-well-formed token bypasses the eager SUPABASE_ACCESS_TOKEN check, so the run // reaches this command's own handler instead of failing generically first. const TEST_TOKEN = "sbp_" + "a".repeat(40); +const TEST_REF = "abcdefghijklmnopqrst"; describe("config pull CLI surface", () => { test("plain `config pull` parses — no boolean flag is accidentally required", async () => { @@ -33,4 +35,58 @@ describe("config pull CLI surface", () => { await rm(cwd, { recursive: true, force: true }); } }); + + test("reads a piped confirmation through the production command layer", async () => { + const cwd = await mkdtemp(join(tmpdir(), "supabase-config-pull-piped-e2e-")); + const server = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + fetch(request) { + const url = new URL(request.url); + if (request.method === "GET" && url.pathname === `/v2/projects/${TEST_REF}/config`) { + return Response.json(v2ProjectConfigResponse({ ref: TEST_REF })); + } + return new Response("not found", { status: 404 }); + }, + }); + try { + const configDir = join(cwd, "supabase"); + const configPath = join(configDir, "config.toml"); + const profilePath = join(cwd, "profile.yaml"); + const before = `project_id = "${TEST_REF}"\n[api]\nmax_rows = 500\n`; + await mkdir(configDir, { recursive: true }); + await writeFile(configPath, before); + await writeFile( + profilePath, + [ + "name: config-pull-piped-e2e", + `api_url: ${JSON.stringify(server.url.origin)}`, + `dashboard_url: ${JSON.stringify(server.url.origin)}`, + 'project_host: "example.invalid"', + "", + ].join("\n"), + ); + + const { exitCode, stdout, stderr } = await runSupabase( + ["config", "pull", "--project-ref", TEST_REF], + { + cwd, + stdin: "n\n", + env: { + SUPABASE_ACCESS_TOKEN: TEST_TOKEN, + SUPABASE_PROFILE: profilePath, + SUPABASE_WORKDIR: cwd, + }, + }, + ); + + expect(exitCode, `${stdout}\n${stderr}`).toBe(0); + expect(stderr).toContain(`Apply 1 change(s) to supabase/config.toml? [Y/n] n\n`); + expect(stdout).toContain("not written (declined)"); + expect(await readFile(configPath, "utf8")).toBe(before); + } finally { + await server.stop(true); + await rm(cwd, { recursive: true, force: true }); + } + }); }); From e1af5d87b75d1cf39696822e7e0e89d48cedbe77 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Sun, 13 Sep 2026 17:13:20 +0530 Subject: [PATCH 2/3] chore(cli): address review nits --- apps/cli/AGENTS.md | 7 ++++--- apps/cli/src/commands/config/pull/pull.e2e.test.ts | 4 +++- .../experimental/stack/destroy/destroy.command.ts | 2 ++ .../experimental/stack/destroy/destroy.e2e.test.ts | 13 +++++++++++++ apps/cli/src/commands/start/start.command.ts | 2 ++ apps/cli/src/shared/cli/run.ts | 2 -- 6 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 apps/cli/src/commands/experimental/stack/destroy/destroy.e2e.test.ts diff --git a/apps/cli/AGENTS.md b/apps/cli/AGENTS.md index d1bd0f6acb..b4de4c127c 100644 --- a/apps/cli/AGENTS.md +++ b/apps/cli/AGENTS.md @@ -68,9 +68,10 @@ Every applicable command must preserve these invariants: [profile loader's schema](src/command-internal/profile-load.ts). Both the [E2E harness](../../packages/cli-test-helpers/src/harness.ts) and [live fixture](tests/helpers/live.ts) depend on file-path mode. -5. Sibling layers in `Layer.mergeAll` each receive required services explicitly. Any production - path that can reach `promptYesNo` provides `stdinLayer`; handler-only tests do not prove this - wiring. Production layer changes require a CLI build and a targeted binary smoke test. +5. Sibling layers in `Layer.mergeAll` each receive required services explicitly. Every production + effect graph retaining `promptYesNo`'s `Stdin` requirement provides `stdinLayer`, even when + runtime guards avoid its non-TTY branch. Paths that can enter that branch require a CLI build + and a targeted piped-input binary test; handler-only tests do not prove this wiring. 6. Honor both output flags. `-o`/`--output` takes precedence over `--output-format`; a new command may reject `--output` with guidance to use `--output-format`, as established by `config diff` and `config pull`. diff --git a/apps/cli/src/commands/config/pull/pull.e2e.test.ts b/apps/cli/src/commands/config/pull/pull.e2e.test.ts index ce8dfd6abe..9a440ccdfe 100644 --- a/apps/cli/src/commands/config/pull/pull.e2e.test.ts +++ b/apps/cli/src/commands/config/pull/pull.e2e.test.ts @@ -81,7 +81,9 @@ describe("config pull CLI surface", () => { ); expect(exitCode, `${stdout}\n${stderr}`).toBe(0); - expect(stderr).toContain(`Apply 1 change(s) to supabase/config.toml? [Y/n] n\n`); + expect(stderr).toContain( + `Apply 1 change(s) to ${join("supabase", "config.toml")}? [Y/n] n\n`, + ); expect(stdout).toContain("not written (declined)"); expect(await readFile(configPath, "utf8")).toBe(before); } finally { diff --git a/apps/cli/src/commands/experimental/stack/destroy/destroy.command.ts b/apps/cli/src/commands/experimental/stack/destroy/destroy.command.ts index 5b6c44198d..8d4754f2c8 100644 --- a/apps/cli/src/commands/experimental/stack/destroy/destroy.command.ts +++ b/apps/cli/src/commands/experimental/stack/destroy/destroy.command.ts @@ -1,6 +1,7 @@ import { Command, Flag } from "effect/unstable/cli"; import type * as CliCommand from "effect/unstable/cli/Command"; import { withJsonErrorHandling } from "../../../../shared/output/json-error-handling.ts"; +import { stdinLayer } from "../../../../shared/runtime/stdin.layer.ts"; import { withCommandTelemetry } from "../../../../telemetry/command-telemetry.ts"; import { stackDestroy } from "./destroy.handler.ts"; @@ -31,4 +32,5 @@ export const stackDestroyCommand = Command.make("destroy", config).pipe( Command.withHandler((flags) => stackDestroy(flags).pipe(withCommandTelemetry({ flags, config }), withJsonErrorHandling), ), + Command.provide(stdinLayer), ); diff --git a/apps/cli/src/commands/experimental/stack/destroy/destroy.e2e.test.ts b/apps/cli/src/commands/experimental/stack/destroy/destroy.e2e.test.ts new file mode 100644 index 0000000000..1f449feb8f --- /dev/null +++ b/apps/cli/src/commands/experimental/stack/destroy/destroy.e2e.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, test } from "vitest"; + +import { runSupabase } from "../../../../../tests/helpers/cli.ts"; + +describe("stack destroy CLI surface", () => { + test("rejects an invalid stack id through the compiled command wiring", () => + runSupabase(["--experimental", "stack", "destroy", "--yes", "--stack-id", "invalid"]).then( + ({ exitCode, stdout, stderr }) => { + expect(exitCode, `${stdout}\n${stderr}`).not.toBe(0); + expect(`${stdout}\n${stderr}`).toContain("--stack-id must be a lowercase SHA-256 stack id"); + }, + )); +}); diff --git a/apps/cli/src/commands/start/start.command.ts b/apps/cli/src/commands/start/start.command.ts index a82506b225..4c8bdbfa57 100644 --- a/apps/cli/src/commands/start/start.command.ts +++ b/apps/cli/src/commands/start/start.command.ts @@ -3,6 +3,7 @@ import { Command, Flag } from "effect/unstable/cli"; import type * as CliCommand from "effect/unstable/cli/Command"; import { commandRuntimeLayer } from "../../shared/runtime/command-runtime.layer.ts"; +import { stdinLayer } from "../../shared/runtime/stdin.layer.ts"; import { withJsonErrorHandling } from "../../shared/output/json-error-handling.ts"; import { httpClientLayer } from "../../auth/http-debug.layer.ts"; import { commandSettingsLayer } from "../../config/command-settings.layer.ts"; @@ -63,4 +64,5 @@ export const startCommand = Command.make("start", config).pipe( start(flags).pipe(withCommandTelemetry({ flags }), withJsonErrorHandling), ), Command.provide(startRuntimeLayer), + Command.provide(stdinLayer), ); diff --git a/apps/cli/src/shared/cli/run.ts b/apps/cli/src/shared/cli/run.ts index b55314207e..f1f36e1e63 100644 --- a/apps/cli/src/shared/cli/run.ts +++ b/apps/cli/src/shared/cli/run.ts @@ -44,7 +44,6 @@ import { ttyLayer } from "../runtime/tty.layer.ts"; import { CommandRuntime } from "../runtime/command-runtime.service.ts"; import { ProcessControl } from "../runtime/process-control.service.ts"; import type { RuntimeInfo } from "../runtime/runtime-info.service.ts"; -import type { Stdin } from "../runtime/stdin.service.ts"; import type { Tty } from "../runtime/tty.service.ts"; import type { Analytics } from "../telemetry/analytics.service.ts"; import { aiToolLayer } from "../telemetry/ai-tool.layer.ts"; @@ -90,7 +89,6 @@ export type AllowedRunCliServices = | TelemetryRuntime | Tty | CommandPlatformApiFactory - | Stdin | "effect/unstable/cli/GlobalFlag/linked" | "effect/unstable/cli/GlobalFlag/local"; From e11a0a81058a47a0dc7e59750a5881edb851ed51 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:37:48 +0530 Subject: [PATCH 3/3] fix(cli): close platform-api factory allow-list leak --- apps/cli/AGENTS.md | 14 +++++--- .../src/commands/config/pull/pull.command.ts | 2 ++ .../stack/destroy/destroy.command.ts | 3 ++ .../stack/destroy/destroy.e2e.test.ts | 13 -------- apps/cli/src/commands/start/SIDE_EFFECTS.md | 7 +++- apps/cli/src/commands/start/start.command.ts | 32 ++++++++++++++++--- apps/cli/src/shared/cli/run.ts | 8 +++-- 7 files changed, 53 insertions(+), 26 deletions(-) delete mode 100644 apps/cli/src/commands/experimental/stack/destroy/destroy.e2e.test.ts diff --git a/apps/cli/AGENTS.md b/apps/cli/AGENTS.md index b4de4c127c..6d5f42ccbd 100644 --- a/apps/cli/AGENTS.md +++ b/apps/cli/AGENTS.md @@ -68,10 +68,16 @@ Every applicable command must preserve these invariants: [profile loader's schema](src/command-internal/profile-load.ts). Both the [E2E harness](../../packages/cli-test-helpers/src/harness.ts) and [live fixture](tests/helpers/live.ts) depend on file-path mode. -5. Sibling layers in `Layer.mergeAll` each receive required services explicitly. Every production - effect graph retaining `promptYesNo`'s `Stdin` requirement provides `stdinLayer`, even when - runtime guards avoid its non-TTY branch. Paths that can enter that branch require a CLI build - and a targeted piped-input binary test; handler-only tests do not prove this wiring. +5. Sibling layers in `Layer.mergeAll` each receive required services explicitly. Production layer + changes require a CLI build and a targeted binary smoke test. Command layer wiring is enforced + by `AllowedRunCliServices` in [src/shared/cli/run.ts](src/shared/cli/run.ts) through the + `CliRootCommand` annotation in [src/cli/root.ts](src/cli/root.ts): a service a command + requires but does not provide fails `tsc` at that annotation. Never widen + `AllowedRunCliServices` to silence that error; provide the layer in the command. + Every production effect graph retaining `promptYesNo`'s `Stdin` requirement provides + `stdinLayer`, even when runtime guards avoid its non-TTY branch. Paths that can enter that + branch require a CLI build and a targeted piped-input binary test; handler-only tests do not + prove this wiring. 6. Honor both output flags. `-o`/`--output` takes precedence over `--output-format`; a new command may reject `--output` with guidance to use `--output-format`, as established by `config diff` and `config pull`. diff --git a/apps/cli/src/commands/config/pull/pull.command.ts b/apps/cli/src/commands/config/pull/pull.command.ts index 07f9e57903..bbdd2882bb 100644 --- a/apps/cli/src/commands/config/pull/pull.command.ts +++ b/apps/cli/src/commands/config/pull/pull.command.ts @@ -76,5 +76,7 @@ export const configPullCommand = Command.make("pull", config).pipe( }, ]), Command.withHandler(configPullHandler), + // `stdinLayer`: the apply confirmation reads piped stdin via `promptYesNo` + // on a non-TTY stdin. Command.provide(Layer.mergeAll(managementApiRuntimeLayer(["config", "pull"]), stdinLayer)), ); diff --git a/apps/cli/src/commands/experimental/stack/destroy/destroy.command.ts b/apps/cli/src/commands/experimental/stack/destroy/destroy.command.ts index 8d4754f2c8..65f186c126 100644 --- a/apps/cli/src/commands/experimental/stack/destroy/destroy.command.ts +++ b/apps/cli/src/commands/experimental/stack/destroy/destroy.command.ts @@ -32,5 +32,8 @@ export const stackDestroyCommand = Command.make("destroy", config).pipe( Command.withHandler((flags) => stackDestroy(flags).pipe(withCommandTelemetry({ flags, config }), withJsonErrorHandling), ), + // `stdinLayer` satisfies `promptYesNo`'s `Stdin` requirement. destroy either rejects a + // non-TTY run up front or short-circuits the prompt via `--yes`, so the layer is here for + // the effect's type requirements only. Command.provide(stdinLayer), ); diff --git a/apps/cli/src/commands/experimental/stack/destroy/destroy.e2e.test.ts b/apps/cli/src/commands/experimental/stack/destroy/destroy.e2e.test.ts deleted file mode 100644 index 1f449feb8f..0000000000 --- a/apps/cli/src/commands/experimental/stack/destroy/destroy.e2e.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { describe, expect, test } from "vitest"; - -import { runSupabase } from "../../../../../tests/helpers/cli.ts"; - -describe("stack destroy CLI surface", () => { - test("rejects an invalid stack id through the compiled command wiring", () => - runSupabase(["--experimental", "stack", "destroy", "--yes", "--stack-id", "invalid"]).then( - ({ exitCode, stdout, stderr }) => { - expect(exitCode, `${stdout}\n${stderr}`).not.toBe(0); - expect(`${stdout}\n${stderr}`).toContain("--stack-id must be a lowercase SHA-256 stack id"); - }, - )); -}); diff --git a/apps/cli/src/commands/start/SIDE_EFFECTS.md b/apps/cli/src/commands/start/SIDE_EFFECTS.md index e182f81a3f..13d662ed23 100644 --- a/apps/cli/src/commands/start/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/start/SIDE_EFFECTS.md @@ -27,7 +27,12 @@ One piece of the old Go CLI's `start` remains explicitly **out of scope**: 1. **Linked-project version-check suggestion** — a best-effort Management API call, made only when a project happens to be linked _and_ the user is logged in, purely to print an "update available" hint. Omitted entirely — this port has zero Management API - dependency for `start`, by design. + dependency for `start`, by design. The runtime layer does compose the lazy + Management-API factory — a static requirement of the shared storage-credentials + resolver whose hosted branch `start` never reaches — so building the layer loads the + credential subsystem: the keyring module import (skipped under `SUPABASE_NO_KEYRING=1` + and on WSL) and the WSL probe's read of `/proc/sys/kernel/osrelease`. No access token + is read or validated, and no Management API call is ever made. ### Fresh-volume DB setup (`startSetupLocalDatabase`) diff --git a/apps/cli/src/commands/start/start.command.ts b/apps/cli/src/commands/start/start.command.ts index 4c8bdbfa57..a45e05076f 100644 --- a/apps/cli/src/commands/start/start.command.ts +++ b/apps/cli/src/commands/start/start.command.ts @@ -5,11 +5,14 @@ import type * as CliCommand from "effect/unstable/cli/Command"; import { commandRuntimeLayer } from "../../shared/runtime/command-runtime.layer.ts"; import { stdinLayer } from "../../shared/runtime/stdin.layer.ts"; import { withJsonErrorHandling } from "../../shared/output/json-error-handling.ts"; +import { commandCredentialsLayer } from "../../auth/command-credentials.layer.ts"; +import { commandPlatformApiFactoryLayer } from "../../auth/command-platform-api-factory.layer.ts"; import { httpClientLayer } from "../../auth/http-debug.layer.ts"; import { commandSettingsLayer } from "../../config/command-settings.layer.ts"; import { dbConnectionLayer } from "../../command-internal/db-connection.layer.ts"; import { debugLoggerLayer } from "../../command-internal/debug-logger.layer.ts"; import { dockerRunLayer } from "../../command-internal/docker-run.layer.ts"; +import { identityStitchLayer } from "../../command-internal/identity-stitch.ts"; import { stringSliceFlag } from "../../command-internal/string-slice-flag.ts"; import { telemetryStateLayer } from "../../telemetry/telemetry-state.layer.ts"; import { withCommandTelemetry } from "../../telemetry/command-telemetry.ts"; @@ -41,12 +44,28 @@ const config = { export type StartFlags = CliCommand.Command.Config.Infer; -// `start` talks directly to Docker with no Management API calls, so it composes its own runtime -// instead of `managementApiRuntimeLayer`. `httpClientLayer` is included explicitly because the -// root runtime doesn't supply `HttpClient.HttpClient`; `dockerRunLayer`/`dbConnectionLayer` back -// the fresh-volume database setup (one-shot migrate jobs plus direct-connection schema SQL). +// `start` talks directly to Docker and makes no Management API calls, so it composes its own +// runtime instead of `managementApiRuntimeLayer`. `httpClientLayer` is included explicitly +// because the root runtime doesn't supply `HttpClient.HttpClient`; +// `dockerRunLayer`/`dbConnectionLayer` back the fresh-volume database setup (one-shot migrate +// jobs plus direct-connection schema SQL). const cliSettings = commandSettingsLayer.pipe(Layer.provide(debugLoggerLayer)); const httpClient = httpClientLayer.pipe(Layer.provide(debugLoggerLayer)); +const credentials = commandCredentialsLayer.pipe( + Layer.provide(cliSettings), + Layer.provide(debugLoggerLayer), +); + +// Exposed because the shared `resolveStorageCredentials` statically requires the (lazy) +// Management-API factory for its hosted branch, even though `start` always passes +// `projectRef: ""` and never reaches it. Mirrors `db reset`'s wiring; laziness keeps the +// local path from ever resolving an access token. +const platformApiFactory = commandPlatformApiFactoryLayer.pipe( + Layer.provide(credentials), + Layer.provide(cliSettings), + Layer.provide(debugLoggerLayer), + Layer.provide(identityStitchLayer), +); const startRuntimeLayer = Layer.mergeAll( cliSettings, @@ -55,6 +74,10 @@ const startRuntimeLayer = Layer.mergeAll( dockerRunLayer, dbConnectionLayer, httpClient, + platformApiFactory, + // `stdinLayer` satisfies `promptYesNo`'s `Stdin` requirement (seed-buckets runs with + // `yes: true`), so `start` never reads a piped line at runtime — type requirements only. + stdinLayer, ); export const startCommand = Command.make("start", config).pipe( @@ -64,5 +87,4 @@ export const startCommand = Command.make("start", config).pipe( start(flags).pipe(withCommandTelemetry({ flags }), withJsonErrorHandling), ), Command.provide(startRuntimeLayer), - Command.provide(stdinLayer), ); diff --git a/apps/cli/src/shared/cli/run.ts b/apps/cli/src/shared/cli/run.ts index f1f36e1e63..d4823afbfd 100644 --- a/apps/cli/src/shared/cli/run.ts +++ b/apps/cli/src/shared/cli/run.ts @@ -21,7 +21,6 @@ import { Credentials } from "../auth/credentials.service.ts"; import type { CliProjectHome } from "../config/cli-project-home.service.ts"; import type { CliSettings } from "../config/cli-settings.service.ts"; import type { ProjectLinkState } from "../config/project-link-state.service.ts"; -import type { CommandPlatformApiFactory } from "../../auth/command-platform-api-factory.service.ts"; import { jsonCliOutputFormatter } from "../output/json-formatter.ts"; import { textCliOutputFormatter } from "../output/text-formatter.ts"; import { outputLayerFor } from "../output/output.layer.ts"; @@ -69,7 +68,11 @@ import { /** * Services available before evaluating the root command. Keep this list explicit: preserving the * root command's requirement channel here makes an accidentally unprovided service fail at the - * shell boundary instead of becoming a runtime missing-service defect. + * shell boundary instead of becoming a runtime missing-service defect. Every entry must be + * genuinely satisfied at the root: by `cliProgramFor`'s provide chain (including + * `fallbackCommandLayer`'s root `CommandRuntime` placeholder) or, for the `GlobalFlag` + * identifiers, by the CLI parser itself. Never widen this union to silence a leaked command + * requirement; provide the layer in the command instead. */ export type AllowedRunCliServices = | Analytics @@ -88,7 +91,6 @@ export type AllowedRunCliServices = | Stdio.Stdio | TelemetryRuntime | Tty - | CommandPlatformApiFactory | "effect/unstable/cli/GlobalFlag/linked" | "effect/unstable/cli/GlobalFlag/local";