diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts index 2b6eb4a8d1..56976af08b 100644 --- a/packages/core/src/flag/flag.ts +++ b/packages/core/src/flag/flag.ts @@ -1,29 +1,75 @@ -import { Config } from "effect" +import { Config, Option } from "effect" + +// altimate_change start — every OPENCODE_* variable is documented under its ALTIMATE_CLI_* +// name (docs/docs/usage/cli.md), so the documented spelling is read first and the OPENCODE_ +// one is the fallback. This is the one definition of that rule: the opencode `Flag` +// namespace and the Effect `Config`-backed services (`effect/config-service.ts`) import it, +// so every read path — this object, that namespace, `RuntimeFlags` — agrees. #1329 was one +// flag that missed the dual read; a cross-check found the table mostly in that state. +// An empty documented value counts as unset: `ALTIMATE_CLI_CONFIG=""` must not hide a real +// `OPENCODE_CONFIG`. +export function documentedAlias(key: string): string | undefined { + return key.startsWith("OPENCODE_") ? "ALTIMATE_CLI_" + key.slice("OPENCODE_".length) : undefined +} + +/** The variable as the environment has it now, documented name first. For the few sites + * that must read at call time rather than through the import-time constants below. */ +export function env(key: string): string | undefined { + const alias = documentedAlias(key) + const documented = alias !== undefined ? process.env[alias] : undefined + return documented !== undefined && documented !== "" ? documented : process.env[key] +} export function truthy(key: string) { - const value = process.env[key]?.toLowerCase() + const value = env(key)?.toLowerCase() return value === "true" || value === "1" } +/** An Effect `Config` boolean with the same documented-name-first rule, for the flags + * that are resolved through the ambient ConfigProvider rather than `process.env`. The + * documented value is read as a string and judged by `truthy`'s rule, so a set-but-invalid + * documented value is `false` — not a fallback to the OPENCODE_ one, which `Config.orElse` + * (and `Config.option`) would silently do, since both swallow parse failures. */ +function bool(key: string) { + const alias = documentedAlias(key) + const fallback = Config.boolean(key).pipe(Config.withDefault(false)) + if (alias === undefined) return fallback + return Config.all({ documented: Config.string(alias).pipe(Config.option), fallback }).pipe( + Config.map(({ documented, fallback }) => { + const value = Option.getOrUndefined(documented) + if (value === undefined || value === "") return fallback + const lower = value.toLowerCase() + return lower === "true" || lower === "1" + }), + ) +} +// altimate_change end + // altimate_change start — dual env var support: ALTIMATE_CLI_* (primary) + OPENCODE_* (fallback). // Re-homed from packages/opencode/src/flag/flag.ts so the extracted TUI (packages/tui, which -// depends on core not opencode) can read the fork flags it uses. +// depends on core not opencode) can read the fork flags it uses. A set documented value wins +// outright — a documented `false` is not overridden by a fallback `true`. function altTruthy(altKey: string, openKey: string) { - return truthy(altKey) || truthy(openKey) + const documented = process.env[altKey] + return documented !== undefined && documented !== "" ? truthy(altKey) : truthy(openKey) } function numberEnv(key: string) { - const value = process.env[key] + const value = env(key) if (!value) return undefined const parsed = Number(value) return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined } // altimate_change end -const copy = process.env["OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"] -const fff = process.env["OPENCODE_DISABLE_FFF"] +// altimate_change start — documented ALTIMATE_CLI_ name read first (see `env`) +const copy = env("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT") +const fff = env("OPENCODE_DISABLE_FFF") +// altimate_change end function enabledByExperimental(key: string) { - return process.env[key] === undefined ? truthy("OPENCODE_EXPERIMENTAL") : truthy(key) + // altimate_change start — documented ALTIMATE_CLI_ name read first (see `env`) + return env(key) === undefined ? truthy("OPENCODE_EXPERIMENTAL") : truthy(key) + // altimate_change end } export const Flag = { @@ -31,9 +77,11 @@ export const Flag = { OTEL_EXPORTER_OTLP_HEADERS: process.env["OTEL_EXPORTER_OTLP_HEADERS"], OPENCODE_AUTO_HEAP_SNAPSHOT: truthy("OPENCODE_AUTO_HEAP_SNAPSHOT"), - OPENCODE_GIT_BASH_PATH: process.env["OPENCODE_GIT_BASH_PATH"], - OPENCODE_CONFIG: process.env["OPENCODE_CONFIG"], - OPENCODE_CONFIG_CONTENT: process.env["OPENCODE_CONFIG_CONTENT"], + // altimate_change start — documented ALTIMATE_CLI_ name read first (see `env`) + OPENCODE_GIT_BASH_PATH: env("OPENCODE_GIT_BASH_PATH"), + OPENCODE_CONFIG: env("OPENCODE_CONFIG"), + OPENCODE_CONFIG_CONTENT: env("OPENCODE_CONFIG_CONTENT"), + // altimate_change end OPENCODE_DISABLE_AUTOUPDATE: truthy("OPENCODE_DISABLE_AUTOUPDATE"), OPENCODE_ALWAYS_NOTIFY_UPDATE: truthy("OPENCODE_ALWAYS_NOTIFY_UPDATE"), OPENCODE_DISABLE_PRUNE: truthy("OPENCODE_DISABLE_PRUNE"), @@ -42,25 +90,29 @@ export const Flag = { OPENCODE_DISABLE_AUTOCOMPACT: truthy("OPENCODE_DISABLE_AUTOCOMPACT"), OPENCODE_DISABLE_MODELS_FETCH: truthy("OPENCODE_DISABLE_MODELS_FETCH"), OPENCODE_DISABLE_MOUSE: truthy("OPENCODE_DISABLE_MOUSE"), - OPENCODE_FAKE_VCS: process.env["OPENCODE_FAKE_VCS"], - OPENCODE_SERVER_PASSWORD: process.env["OPENCODE_SERVER_PASSWORD"], - OPENCODE_SERVER_USERNAME: process.env["OPENCODE_SERVER_USERNAME"], + // altimate_change start — documented ALTIMATE_CLI_ name read first (see `env`) + OPENCODE_FAKE_VCS: env("OPENCODE_FAKE_VCS"), + OPENCODE_SERVER_PASSWORD: env("OPENCODE_SERVER_PASSWORD"), + OPENCODE_SERVER_USERNAME: env("OPENCODE_SERVER_USERNAME"), + // altimate_change end OPENCODE_DISABLE_FFF: fff === undefined ? process.platform === "win32" : truthy("OPENCODE_DISABLE_FFF"), // Experimental - OPENCODE_EXPERIMENTAL_FILEWATCHER: Config.boolean("OPENCODE_EXPERIMENTAL_FILEWATCHER").pipe( - Config.withDefault(false), - ), - OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: Config.boolean("OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER").pipe( - Config.withDefault(false), - ), + // altimate_change start — documented ALTIMATE_CLI_ name read first (see `bool`) + OPENCODE_EXPERIMENTAL_FILEWATCHER: bool("OPENCODE_EXPERIMENTAL_FILEWATCHER"), + OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: bool("OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER"), + // altimate_change end OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT: copy === undefined ? process.platform === "win32" : truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"), - OPENCODE_MODELS_URL: process.env["OPENCODE_MODELS_URL"], - OPENCODE_MODELS_PATH: process.env["OPENCODE_MODELS_PATH"], - OPENCODE_DB: process.env["OPENCODE_DB"], + // altimate_change start — documented ALTIMATE_CLI_ name read first (see `env`) + OPENCODE_MODELS_URL: env("OPENCODE_MODELS_URL"), + OPENCODE_MODELS_PATH: env("OPENCODE_MODELS_PATH"), + OPENCODE_DB: env("OPENCODE_DB"), + // altimate_change end - OPENCODE_WORKSPACE_ID: process.env["OPENCODE_WORKSPACE_ID"], + // altimate_change start — documented ALTIMATE_CLI_ name read first (see `env`) + OPENCODE_WORKSPACE_ID: env("OPENCODE_WORKSPACE_ID"), + // altimate_change end // Unrelated to ALTIMATE_WORKSPACE (SaaS project-binding pilot) — this gates upstream's multi-instance/worktree control plane. OPENCODE_EXPERIMENTAL_WORKSPACES: enabledByExperimental("OPENCODE_EXPERIMENTAL_WORKSPACES"), @@ -94,24 +146,28 @@ export const Flag = { get OPENCODE_EXPERIMENTAL_REFERENCES() { return enabledByExperimental("OPENCODE_EXPERIMENTAL_REFERENCES") }, + // altimate_change start — documented ALTIMATE_CLI_ name read first (see `env`) get OPENCODE_TUI_CONFIG() { - return process.env["OPENCODE_TUI_CONFIG"] + return env("OPENCODE_TUI_CONFIG") }, get OPENCODE_CONFIG_DIR() { - return process.env["OPENCODE_CONFIG_DIR"] + return env("OPENCODE_CONFIG_DIR") }, + // altimate_change end get OPENCODE_PURE() { return truthy("OPENCODE_PURE") }, + // altimate_change start — documented ALTIMATE_CLI_ name read first (see `env`) get OPENCODE_PERMISSION() { - return process.env["OPENCODE_PERMISSION"] + return env("OPENCODE_PERMISSION") }, get OPENCODE_PLUGIN_META_FILE() { - return process.env["OPENCODE_PLUGIN_META_FILE"] + return env("OPENCODE_PLUGIN_META_FILE") }, get OPENCODE_CLIENT() { - return process.env["OPENCODE_CLIENT"] ?? "cli" + return env("OPENCODE_CLIENT") ?? "cli" }, + // altimate_change end // altimate_change start — fork flags used by the extracted TUI (packages/tui). Getters so the // runtime-set yolo flag (set by --yolo middleware after module load) evaluates at access time. get ALTIMATE_CALM_MODE() { @@ -131,12 +187,13 @@ export const Flag = { ) }, get ALTIMATE_CLI_YOLO() { + // Empty counts as unset, as everywhere else in this file. const alt = process.env["ALTIMATE_CLI_YOLO"] - if (alt !== undefined) { + if (alt !== undefined && alt !== "") { const v = alt.toLowerCase() return v === "true" || v === "1" } - const oc = process.env["OPENCODE_YOLO"]?.toLowerCase() + const oc = env("OPENCODE_YOLO")?.toLowerCase() return oc === "true" || oc === "1" }, // altimate_change end diff --git a/packages/opencode/src/altimate/tools/project-scan.ts b/packages/opencode/src/altimate/tools/project-scan.ts index b98a89d2e6..ca0cb1551a 100644 --- a/packages/opencode/src/altimate/tools/project-scan.ts +++ b/packages/opencode/src/altimate/tools/project-scan.ts @@ -8,6 +8,9 @@ import { Telemetry } from "@/telemetry" import * as OnboardingTelemetry from "@/altimate/telemetry/onboarding" import { Config } from "@/config/config" import { Skill } from "../../skill" +// altimate_change start — documented ALTIMATE_CLI_ name read first (see core `truthy`) +import { truthy as FlagEnvTruthy } from "@opencode-ai/core/flag/flag" +// altimate_change end // --- Types --- @@ -869,10 +872,8 @@ export const ProjectScanTool = Tool.define("project_scan", { // Upstream's Effect-logging migration removed these keys from the Flag namespace // (and turned FILEWATCHER into an Effect Config). For census we only need // "was the env var set", so read process.env directly. - const flagSet = (key: string) => { - const v = process.env[key] - return v === "true" || v === "1" - } + // Through core's `truthy`, so the documented ALTIMATE_CLI_ name is counted too. + const flagSet = (key: string) => FlagEnvTruthy(key) const enabledFlags: string[] = [] if (flagSet("OPENCODE_EXPERIMENTAL")) enabledFlags.push("experimental") if (flagSet("OPENCODE_EXPERIMENTAL_PLAN_MODE")) enabledFlags.push("plan_mode") diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 3ea9ddb91b..a31a36ab86 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -9,6 +9,9 @@ import { Telemetry } from "../../altimate/telemetry" // altimate_change start — workspace feature gate (see the flush after loopPromise) import { Flag as CoreFlag } from "@opencode-ai/core/flag/flag" // altimate_change end +// altimate_change start — runtime env read with the documented-name rule +import { env as FlagEnv } from "@opencode-ai/core/flag/flag" +// altimate_change end import { bootstrap } from "../bootstrap" import { EOL } from "os" import { Filesystem } from "../../util/filesystem" @@ -1508,9 +1511,11 @@ You are speaking to a non-technical business executive. Follow these rules stric if (args.attach) { const headers = (() => { - const password = args.password ?? process.env.OPENCODE_SERVER_PASSWORD + // altimate_change start — documented ALTIMATE_CLI_SERVER_* names read first (core `env`) + const password = args.password ?? FlagEnv("OPENCODE_SERVER_PASSWORD") if (!password) return undefined - const username = process.env.OPENCODE_SERVER_USERNAME ?? "opencode" + const username = FlagEnv("OPENCODE_SERVER_USERNAME") ?? "opencode" + // altimate_change end const auth = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}` return { Authorization: auth } })() diff --git a/packages/opencode/src/cli/upgrade.ts b/packages/opencode/src/cli/upgrade.ts index a6c1d46962..81bfdb0e7f 100644 --- a/packages/opencode/src/cli/upgrade.ts +++ b/packages/opencode/src/cli/upgrade.ts @@ -4,6 +4,9 @@ import { Flag } from "@opencode-ai/core/flag/flag" import { Installation } from "@/installation" import { InstallationVersion, InstallationChannel, isPublishableChannel } from "@opencode-ai/core/installation/version" import { GlobalBus } from "@/bus/global" +// altimate_change start — documented ALTIMATE_CLI_ name read first (see core `truthy`) +import { truthy as FlagEnvTruthy } from "@opencode-ai/core/flag/flag" +// altimate_change end // altimate_change start — re-export the centralized channel guard so existing importers // (cli/cmd/upgrade.ts, installation/upgrade.test.ts) keep resolving it from here. export { isPublishableChannel } @@ -85,13 +88,10 @@ export function isValidVersion(version: string): boolean { // altimate_change end // altimate_change start — upstream_fix: honor both fork and upstream autoupdate-disable env vars -function truthyEnv(name: string) { - const value = process.env[name]?.toLowerCase() - return value === "true" || value === "1" -} - export function isAutoupdateDisabledByEnv() { - return truthyEnv("ALTIMATE_CLI_DISABLE_AUTOUPDATE") || truthyEnv("OPENCODE_DISABLE_AUTOUPDATE") + // The documented name wins outright when set (a documented `false` is not overridden + // by a fallback `true`), same as every `Flag.*` read. + return FlagEnvTruthy("OPENCODE_DISABLE_AUTOUPDATE") } // altimate_change end diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 23dc88ac78..a533596b22 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -9,6 +9,9 @@ import { mergeDeep } from "remeda" import { Global } from "../global" import fsNode from "fs/promises" import { Flag } from "@opencode-ai/core/flag/flag" +// altimate_change start — runtime env read with the documented-name rule +import { env as FlagEnv } from "@opencode-ai/core/flag/flag" +// altimate_change end import { Auth } from "../auth" import { Env } from "../env" import { applyEdits, modify } from "jsonc-parser" @@ -577,12 +580,17 @@ export const layer = Layer.effect( yield* mergePluginOrigins(dir, list) } - if (process.env.OPENCODE_CONFIG_CONTENT) { + // altimate_change start — documented ALTIMATE_CLI_CONFIG_CONTENT read first (core `env`) + const configContent = FlagEnv("OPENCODE_CONFIG_CONTENT") + if (configContent) { + // altimate_change end const source = "OPENCODE_CONFIG_CONTENT" // altimate_change start — upstream_fix (#701): clear before this load. ConfigVariable.resetBlankedEnvVars(source) // altimate_change end - const next = yield* loadConfig(process.env.OPENCODE_CONFIG_CONTENT, { + // altimate_change start — see above + const next = yield* loadConfig(configContent, { + // altimate_change end dir: ctx.directory, source, }) diff --git a/packages/opencode/src/effect/config-service.ts b/packages/opencode/src/effect/config-service.ts index 3c13afc12a..ad341a9276 100644 --- a/packages/opencode/src/effect/config-service.ts +++ b/packages/opencode/src/effect/config-service.ts @@ -1,4 +1,7 @@ -import { Config, Context, Effect, Layer } from "effect" +import { Config, ConfigProvider, Context, Effect, Layer } from "effect" +// altimate_change start — every OPENCODE_* variable is documented under an ALTIMATE_CLI_* name +import { documentedAlias } from "@opencode-ai/core/flag/flag" +// altimate_change end type ConfigMap = Record> @@ -52,7 +55,19 @@ export const Service = return Layer.effect( tag, Effect.gen(function* () { - const config = yield* Config.all(fields) + // altimate_change start — resolve the documented ALTIMATE_CLI_* name before the + // OPENCODE_* one, whatever provider is active. Same rule as `flag.ts`'s `read`; + // without it a documented name reached `Flag.*` but never a Config-backed field, + // and `RuntimeFlags.disableExternalSkills` — the real skill-discovery gate — kept + // ignoring `ALTIMATE_CLI_DISABLE_EXTERNAL_SKILLS` (#1329). Built with `make` + // rather than `orElse`, whose fallback calls `get` and skips `mapInput`. + const config = yield* Config.all(fields).pipe( + Effect.provideServiceEffect( + ConfigProvider.ConfigProvider, + Effect.map(ConfigProvider.ConfigProvider, aliasDocumentedNames), + ), + ) + // altimate_change end // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- Config.all preserves the field shape, but its conditional return type also supports iterable inputs. return tag.of(config as Shape) }), @@ -65,3 +80,21 @@ export const Service = } export * as ConfigService from "./config-service" + +// altimate_change start — see `defaultLayer` +/** A provider that tries the documented `ALTIMATE_CLI_*` spelling of an `OPENCODE_*` path + * first and falls back to the path as written. An empty documented value counts as unset. */ +export function aliasDocumentedNames(provider: ConfigProvider.ConfigProvider): ConfigProvider.ConfigProvider { + return ConfigProvider.make((path) => { + const head = path[0] + const alias = typeof head === "string" ? documentedAlias(head) : undefined + if (alias === undefined) return provider.load(path) + // Only a set, non-empty scalar is a documented value. `fromEnv` also answers a + // prefix path with a Record node (`ALTIMATE_CLI_CLIENT_CHILD=x` makes + // `ALTIMATE_CLI_CLIENT` one), which is not a value of the flag. + return Effect.flatMap(provider.load([alias, ...path.slice(1)]), (node) => + node && node._tag === "Value" && node.value !== "" ? Effect.succeed(node) : provider.load(path), + ) + }) +} +// altimate_change end diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index f20e1d0444..a833dedff5 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -1,20 +1,34 @@ +// altimate_change start — every OPENCODE_* variable is documented under its ALTIMATE_CLI_* name +// (docs/docs/usage/cli.md), so the documented spelling is read first and the OPENCODE_ one is +// the fallback. Done once here rather than per flag: #1329 was one flag that missed the dual +// read, and a cross-check found most of the table in the same state. Non-OPENCODE_ keys are +// read as-is. +// The rule itself lives in core's `flag/flag.ts` (`env`), which the core `Flag` object, this +// namespace and the Effect `Config`-backed services (`effect/config-service.ts`) all share. +import { env as read } from "@opencode-ai/core/flag/flag" +// altimate_change end + function truthy(key: string) { - const value = process.env[key]?.toLowerCase() + const value = read(key)?.toLowerCase() return value === "true" || value === "1" } function falsy(key: string) { - const value = process.env[key]?.toLowerCase() + const value = read(key)?.toLowerCase() return value === "false" || value === "0" } -// altimate_change start - dual env var support: ALTIMATE_CLI_* (primary) + OPENCODE_* (fallback) +// altimate_change start - dual env var support: ALTIMATE_CLI_* (primary) + OPENCODE_* (fallback). +// Both go through `read`, so a documented value wins outright (a documented `false` is not +// overridden by a fallback `true`) — the one precedence rule for every paired flag. function altTruthy(altKey: string, openKey: string) { - return truthy(altKey) || truthy(openKey) + const documented = process.env[altKey] + return documented !== undefined && documented !== "" ? truthy(altKey) : truthy(openKey) } function altEnv(altKey: string, openKey: string) { - return process.env[altKey] ?? process.env[openKey] + const documented = process.env[altKey] + return documented !== undefined && documented !== "" ? documented : read(openKey) } // altimate_change end @@ -40,11 +54,15 @@ export namespace Flag { export declare const OPENCODE_PURE: boolean // altimate_change end export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE") - export const OPENCODE_GIT_BASH_PATH = process.env["OPENCODE_GIT_BASH_PATH"] - export const OPENCODE_CONFIG = process.env["OPENCODE_CONFIG"] + // altimate_change start — documented ALTIMATE_CLI_ name read first (see `read`) + export const OPENCODE_GIT_BASH_PATH = read("OPENCODE_GIT_BASH_PATH") + export const OPENCODE_CONFIG = read("OPENCODE_CONFIG") + // altimate_change end export declare const OPENCODE_TUI_CONFIG: string | undefined export declare const OPENCODE_CONFIG_DIR: string | undefined - export const OPENCODE_CONFIG_CONTENT = process.env["OPENCODE_CONFIG_CONTENT"] + // altimate_change start — documented ALTIMATE_CLI_ name read first (see `read`) + export const OPENCODE_CONFIG_CONTENT = read("OPENCODE_CONFIG_CONTENT") + // altimate_change end // altimate_change start — support ALTIMATE_CLI_DISABLE_AUTOUPDATE env var (documented name) export const OPENCODE_DISABLE_AUTOUPDATE = altTruthy("ALTIMATE_CLI_DISABLE_AUTOUPDATE", "OPENCODE_DISABLE_AUTOUPDATE") // altimate_change end @@ -88,7 +106,9 @@ export namespace Flag { } // altimate_change end export const OPENCODE_DISABLE_TERMINAL_TITLE = truthy("OPENCODE_DISABLE_TERMINAL_TITLE") - export const OPENCODE_PERMISSION = process.env["OPENCODE_PERMISSION"] + // altimate_change start — documented ALTIMATE_CLI_ name read first (see `read`) + export const OPENCODE_PERMISSION = read("OPENCODE_PERMISSION") + // altimate_change end export const OPENCODE_DISABLE_DEFAULT_PLUGINS = truthy("OPENCODE_DISABLE_DEFAULT_PLUGINS") export const OPENCODE_DISABLE_LSP_DOWNLOAD = truthy("OPENCODE_DISABLE_LSP_DOWNLOAD") export const OPENCODE_ENABLE_EXPERIMENTAL_MODELS = truthy("OPENCODE_ENABLE_EXPERIMENTAL_MODELS") @@ -102,10 +122,14 @@ export namespace Flag { export const OPENCODE_DISABLE_EXTERNAL_SKILLS = OPENCODE_DISABLE_CLAUDE_CODE_SKILLS || truthy("OPENCODE_DISABLE_EXTERNAL_SKILLS") export declare const OPENCODE_DISABLE_PROJECT_CONFIG: boolean - export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"] + // altimate_change start — documented ALTIMATE_CLI_ name read first (see `read`) + export const OPENCODE_FAKE_VCS = read("OPENCODE_FAKE_VCS") + // altimate_change end export declare const OPENCODE_CLIENT: string - export const OPENCODE_SERVER_PASSWORD = process.env["OPENCODE_SERVER_PASSWORD"] - export const OPENCODE_SERVER_USERNAME = process.env["OPENCODE_SERVER_USERNAME"] + // altimate_change start — documented ALTIMATE_CLI_ name read first (see `read`) + export const OPENCODE_SERVER_PASSWORD = read("OPENCODE_SERVER_PASSWORD") + export const OPENCODE_SERVER_USERNAME = read("OPENCODE_SERVER_USERNAME") + // altimate_change end export const OPENCODE_ENABLE_QUESTION_TOOL = truthy("OPENCODE_ENABLE_QUESTION_TOOL") // Experimental @@ -115,7 +139,9 @@ export namespace Flag { export const OPENCODE_EXPERIMENTAL_ICON_DISCOVERY = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY") - const copy = process.env["OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"] + // altimate_change start — documented ALTIMATE_CLI_ name read first (see `read`) + const copy = read("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT") + // altimate_change end export const OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT = copy === undefined ? process.platform === "win32" : truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT") export const OPENCODE_ENABLE_EXA = @@ -141,14 +167,18 @@ export namespace Flag { number("OPENCODE_CONTENT_MAX_WIDTH") ?? (ALTIMATE_CALM_MODE ? 100 : undefined) // altimate_change end - export const OPENCODE_MODELS_URL = process.env["OPENCODE_MODELS_URL"] - export const OPENCODE_MODELS_PATH = process.env["OPENCODE_MODELS_PATH"] + // altimate_change start — documented ALTIMATE_CLI_ name read first (see `read`) + export const OPENCODE_MODELS_URL = read("OPENCODE_MODELS_URL") + export const OPENCODE_MODELS_PATH = read("OPENCODE_MODELS_PATH") + // altimate_change end export const OPENCODE_DISABLE_CHANNEL_DB = truthy("OPENCODE_DISABLE_CHANNEL_DB") export const OPENCODE_SKIP_MIGRATIONS = truthy("OPENCODE_SKIP_MIGRATIONS") export const OPENCODE_STRICT_CONFIG_DEPS = truthy("OPENCODE_STRICT_CONFIG_DEPS") function number(key: string) { - const value = process.env[key] + // altimate_change start — documented ALTIMATE_CLI_ name read first (see `read`) + const value = read(key) + // altimate_change end if (!value) return undefined const parsed = Number(value) return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined @@ -171,7 +201,9 @@ Object.defineProperty(Flag, "OPENCODE_DISABLE_PROJECT_CONFIG", { // because tests and external tooling may set this env var at runtime Object.defineProperty(Flag, "OPENCODE_TUI_CONFIG", { get() { - return process.env["OPENCODE_TUI_CONFIG"] + // altimate_change start — documented ALTIMATE_CLI_ name read first (see `read`) + return read("OPENCODE_TUI_CONFIG") + // altimate_change end }, enumerable: true, configurable: false, @@ -182,7 +214,9 @@ Object.defineProperty(Flag, "OPENCODE_TUI_CONFIG", { // because external tooling may set this env var at runtime Object.defineProperty(Flag, "OPENCODE_CONFIG_DIR", { get() { - return process.env["OPENCODE_CONFIG_DIR"] + // altimate_change start — documented ALTIMATE_CLI_ name read first (see `read`) + return read("OPENCODE_CONFIG_DIR") + // altimate_change end }, enumerable: true, configurable: false, @@ -193,7 +227,9 @@ Object.defineProperty(Flag, "OPENCODE_CONFIG_DIR", { // because some commands override the client at runtime Object.defineProperty(Flag, "OPENCODE_CLIENT", { get() { - return process.env["OPENCODE_CLIENT"] ?? "cli" + // altimate_change start — documented ALTIMATE_CLI_ name read first (see `read`) + return read("OPENCODE_CLIENT") ?? "cli" + // altimate_change end }, enumerable: true, configurable: false, @@ -204,11 +240,11 @@ Object.defineProperty(Flag, "OPENCODE_CLIENT", { Object.defineProperty(Flag, "ALTIMATE_CLI_YOLO", { get() { const alt = process.env["ALTIMATE_CLI_YOLO"] - if (alt !== undefined) { + if (alt !== undefined && alt !== "") { const v = alt.toLowerCase() return v === "true" || v === "1" } - const oc = process.env["OPENCODE_YOLO"]?.toLowerCase() + const oc = read("OPENCODE_YOLO")?.toLowerCase() return oc === "true" || oc === "1" }, enumerable: true, @@ -229,7 +265,7 @@ Object.defineProperty(Flag, "ALTIMATE_RUN_MODE", { // altimate_change start - ALTIMATE_CLI_CLIENT with OPENCODE_CLIENT fallback Object.defineProperty(Flag, "ALTIMATE_CLI_CLIENT", { get() { - return process.env["ALTIMATE_CLI_CLIENT"] ?? process.env["OPENCODE_CLIENT"] ?? "cli" + return process.env["ALTIMATE_CLI_CLIENT"] ?? read("OPENCODE_CLIENT") ?? "cli" }, enumerable: true, configurable: false, @@ -256,7 +292,7 @@ Object.defineProperty(Flag, "OTEL_EXPORTER_OTLP_HEADERS", { }) Object.defineProperty(Flag, "OPENCODE_AUTO_HEAP_SNAPSHOT", { get() { - const v = process.env["OPENCODE_AUTO_HEAP_SNAPSHOT"]?.toLowerCase() + const v = read("OPENCODE_AUTO_HEAP_SNAPSHOT")?.toLowerCase() return v === "true" || v === "1" }, enumerable: true, @@ -264,14 +300,14 @@ Object.defineProperty(Flag, "OPENCODE_AUTO_HEAP_SNAPSHOT", { }) Object.defineProperty(Flag, "OPENCODE_PLUGIN_META_FILE", { get() { - return process.env["OPENCODE_PLUGIN_META_FILE"] + return read("OPENCODE_PLUGIN_META_FILE") }, enumerable: true, configurable: false, }) Object.defineProperty(Flag, "OPENCODE_DISABLE_EMBEDDED_WEB_UI", { get() { - const v = process.env["OPENCODE_DISABLE_EMBEDDED_WEB_UI"]?.toLowerCase() + const v = read("OPENCODE_DISABLE_EMBEDDED_WEB_UI")?.toLowerCase() return v === "true" || v === "1" }, enumerable: true, @@ -279,7 +315,7 @@ Object.defineProperty(Flag, "OPENCODE_DISABLE_EMBEDDED_WEB_UI", { }) Object.defineProperty(Flag, "OPENCODE_PURE", { get() { - const v = process.env["OPENCODE_PURE"]?.toLowerCase() + const v = read("OPENCODE_PURE")?.toLowerCase() return v === "true" || v === "1" }, enumerable: true, diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index ef31cf4c4e..a6d9154361 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -17,6 +17,9 @@ import { Flag } from "../flag/flag" import { iife } from "@/util/iife" import { Effect } from "effect" import freshSchema from "@opencode-ai/core/database/schema.gen" +// altimate_change start — documented ALTIMATE_CLI_ name read first (see core `env`) +import { env as FlagEnv } from "@opencode-ai/core/flag/flag" +// altimate_change end declare const OPENCODE_MIGRATIONS: { sql: string; timestamp: number; name: string }[] | undefined @@ -33,7 +36,9 @@ export namespace Database { export const Path = iife(() => { // altimate_change upstream_fix — keep legacy storage on the same sqlite file // as core when callers override OPENCODE_DB. - const overridden = process.env["OPENCODE_DB"] + // Read through the documented-name rule like core's `Flag.OPENCODE_DB`: the two + // must name the same file or one process splits across two databases. + const overridden = FlagEnv("OPENCODE_DB") if (overridden) { if (overridden === ":memory:" || path.isAbsolute(overridden)) return overridden return path.join(Global.Path.data, overridden) diff --git a/packages/opencode/test/cli/upgrade-decision.test.ts b/packages/opencode/test/cli/upgrade-decision.test.ts index 8d25474bf4..d399cf15ef 100644 --- a/packages/opencode/test/cli/upgrade-decision.test.ts +++ b/packages/opencode/test/cli/upgrade-decision.test.ts @@ -186,6 +186,11 @@ describe("isAutoupdateDisabledByEnv", () => { withAutoupdateEnv({ ALTIMATE_CLI_DISABLE_AUTOUPDATE: "0", OPENCODE_DISABLE_AUTOUPDATE: "false" }, () => { expect(isAutoupdateDisabledByEnv()).toBe(false) })) + + test("a documented false is not overridden by a fallback true — same rule as Flag.* (#1329 class)", () => + withAutoupdateEnv({ ALTIMATE_CLI_DISABLE_AUTOUPDATE: "false", OPENCODE_DISABLE_AUTOUPDATE: "true" }, () => { + expect(isAutoupdateDisabledByEnv()).toBe(false) + })) }) // altimate_change end diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 9502869ff7..cd2a3a9d6d 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -2240,6 +2240,37 @@ describe("OPENCODE_PERMISSION env var", () => { ) }) +// altimate_change start — the documented ALTIMATE_CLI_CONFIG_CONTENT name reaches the +// same load path (#1329 class): this is the site that read `process.env` directly. +describe("ALTIMATE_CLI_CONFIG_CONTENT", () => { + it.instance("is loaded like OPENCODE_CONFIG_CONTENT", () => + withProcessEnv( + "ALTIMATE_CLI_CONFIG_CONTENT", + JSON.stringify({ $schema: "https://opencode.ai/config.json", username: "documented-name" }), + Effect.gen(function* () { + const config = yield* Config.use.get() + expect(config.username).toBe("documented-name") + }), + ), + ) + + it.instance("wins over OPENCODE_CONFIG_CONTENT when both are set", () => + withProcessEnv( + "ALTIMATE_CLI_CONFIG_CONTENT", + JSON.stringify({ $schema: "https://opencode.ai/config.json", username: "documented-name" }), + withProcessEnv( + "OPENCODE_CONFIG_CONTENT", + JSON.stringify({ $schema: "https://opencode.ai/config.json", username: "fallback-name" }), + Effect.gen(function* () { + const config = yield* Config.use.get() + expect(config.username).toBe("documented-name") + }), + ), + ), + ) +}) +// altimate_change end + describe("OPENCODE_CONFIG_CONTENT token substitution", () => { it.instance("substitutes {env:} tokens in OPENCODE_CONFIG_CONTENT", () => withProcessEnv( diff --git a/packages/opencode/test/flag/external-skills-flag.test.ts b/packages/opencode/test/flag/external-skills-flag.test.ts new file mode 100644 index 0000000000..f3c427b899 --- /dev/null +++ b/packages/opencode/test/flag/external-skills-flag.test.ts @@ -0,0 +1,209 @@ +import { describe, expect, test } from "bun:test" +import path from "node:path" +import { Config, ConfigProvider, Effect, Layer, Option } from "effect" +import { ConfigService } from "../../src/effect/config-service" +import { RuntimeFlags } from "../../src/effect/runtime-flags" +import * as ServerAuth from "../../src/server/auth" + +// The flag module reads the environment at import time, so each case loads it in a fresh +// subprocess with exactly the variables under test. Regression for #1329: the documented +// ALTIMATE_CLI_DISABLE_EXTERNAL_SKILLS did nothing because only the OPENCODE_ spelling was +// read — and a cross-check found most of the documented table in the same state, so the +// second describe walks the table itself. +async function flag(name: string, env: Record): Promise { + const script = `import { Flag } from "./src/flag/flag"; console.log(JSON.stringify(Flag.${name} ?? null))` + const proc = Bun.spawn(["bun", "-e", script], { + cwd: path.resolve(import.meta.dir, "../.."), + env: { PATH: process.env.PATH!, HOME: process.env.HOME!, NODE_OPTIONS: "", ...env }, + stdout: "pipe", + stderr: "pipe", + }) + const out = await new Response(proc.stdout).text() + const code = await proc.exited + if (code !== 0) throw new Error(await new Response(proc.stderr).text()) + return JSON.parse(out.trim()) +} + +describe("external-skills flags read the documented ALTIMATE_CLI_ names (#1329)", () => { + test("ALTIMATE_CLI_DISABLE_EXTERNAL_SKILLS alone disables external skills", async () => { + expect(await flag("OPENCODE_DISABLE_EXTERNAL_SKILLS", { ALTIMATE_CLI_DISABLE_EXTERNAL_SKILLS: "true" })).toBe(true) + }) + + test("the OPENCODE_ spelling still works", async () => { + expect(await flag("OPENCODE_DISABLE_EXTERNAL_SKILLS", { OPENCODE_DISABLE_EXTERNAL_SKILLS: "1" })).toBe(true) + }) + + test("unset is off; an explicit false is off; the documented name wins over the fallback", async () => { + expect(await flag("OPENCODE_DISABLE_EXTERNAL_SKILLS", {})).toBe(false) + expect(await flag("OPENCODE_DISABLE_EXTERNAL_SKILLS", { ALTIMATE_CLI_DISABLE_EXTERNAL_SKILLS: "false" })).toBe(false) + expect( + await flag("OPENCODE_CONFIG", { ALTIMATE_CLI_CONFIG: "/documented.json", OPENCODE_CONFIG: "/fallback.json" }), + ).toBe("/documented.json") + }) + + test("the CLAUDE_CODE family accepts the ALTIMATE_CLI_ names too, and the parent implies the children", async () => { + expect(await flag("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS", { ALTIMATE_CLI_DISABLE_CLAUDE_CODE_SKILLS: "true" })).toBe(true) + expect(await flag("OPENCODE_DISABLE_EXTERNAL_SKILLS", { ALTIMATE_CLI_DISABLE_CLAUDE_CODE: "true" })).toBe(true) + expect(await flag("OPENCODE_DISABLE_CLAUDE_CODE_PROMPT", { ALTIMATE_CLI_DISABLE_CLAUDE_CODE_PROMPT: "true" })).toBe(true) + }) +}) + +describe("every ALTIMATE_CLI_ variable the CLI docs table lists reaches its flag", () => { + // Names in docs/docs/usage/cli.md. A row here without a reading flag is a docs bug or a + // flag bug; either way the documented variable would silently do nothing. + const numeric = new Set(["ALTIMATE_CLI_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS", "ALTIMATE_CLI_EXPERIMENTAL_OUTPUT_TOKEN_MAX"]) + const strings = new Set([ + "ALTIMATE_CLI_CONFIG", + "ALTIMATE_CLI_CONFIG_CONTENT", + "ALTIMATE_CLI_CONFIG_DIR", + "ALTIMATE_CLI_GIT_BASH_PATH", + "ALTIMATE_CLI_PERMISSION", + "ALTIMATE_CLI_SERVER_PASSWORD", + "ALTIMATE_CLI_SERVER_USERNAME", + ]) + // Flags whose export is not simply OPENCODE_. + const exportFor = (name: string) => (name === "ALTIMATE_CLI_YOLO" ? "ALTIMATE_CLI_YOLO" : "OPENCODE_" + name.slice("ALTIMATE_CLI_".length)) + + test("table-driven", async () => { + const docs = await Bun.file(path.resolve(import.meta.dir, "../../../../docs/docs/usage/cli.md")).text() + const names = [...new Set(docs.match(/ALTIMATE_CLI_[A-Z_]+/g) ?? [])].filter((n) => n !== "ALTIMATE_CLI_CLIENT") + expect(names.length).toBeGreaterThan(15) + const missing: string[] = [] + for (const name of names) { + const value = numeric.has(name) ? "4321" : strings.has(name) ? "documented-value" : "true" + const expected = numeric.has(name) ? 4321 : strings.has(name) ? "documented-value" : true + const got = await flag(exportFor(name), { [name]: value }) + if (got !== expected) missing.push(`${name} -> Flag.${exportFor(name)} = ${JSON.stringify(got)}`) + } + expect(missing).toEqual([]) + }) +}) + +// The gate skill discovery really reads is `RuntimeFlags.disableExternalSkills`, an Effect +// `Config` resolved through the ambient ConfigProvider — not `Flag.*`. The first cut of this +// fix aliased only `Flag.*`, so the documented name still did nothing where it mattered. +// (cubic, #1341) +describe("the Effect Config-backed flags read the documented names too", () => { + const runtimeFlags = (env: Record) => + Effect.runPromise( + Effect.gen(function* () { + return yield* RuntimeFlags.Service + }).pipe( + Effect.provide( + RuntimeFlags.Service.defaultLayer.pipe(Layer.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env })))), + ), + ), + ) + const serverAuth = (env: Record) => + Effect.runPromise( + Effect.gen(function* () { + return yield* ServerAuth.Config + }).pipe( + Effect.provide( + ServerAuth.Config.defaultLayer.pipe(Layer.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env })))), + ), + ), + ) + + test("ALTIMATE_CLI_DISABLE_EXTERNAL_SKILLS alone reaches RuntimeFlags.disableExternalSkills", async () => { + expect((await runtimeFlags({ ALTIMATE_CLI_DISABLE_EXTERNAL_SKILLS: "true" })).disableExternalSkills).toBe(true) + expect((await runtimeFlags({ ALTIMATE_CLI_DISABLE_CLAUDE_CODE: "true" })).disableExternalSkills).toBe(true) + expect((await runtimeFlags({})).disableExternalSkills).toBe(false) + }) + + test("the documented value wins outright, and an empty documented value is unset", async () => { + const both = await runtimeFlags({ ALTIMATE_CLI_DISABLE_EXTERNAL_SKILLS: "false", OPENCODE_DISABLE_EXTERNAL_SKILLS: "true" }) + expect(both.disableExternalSkills).toBe(false) + const empty = await runtimeFlags({ ALTIMATE_CLI_CLIENT: "", OPENCODE_CLIENT: "vscode" }) + expect(empty.client).toBe("vscode") + }) + + test("a numeric flag works, and a non-OPENCODE path or a prefix-only documented name is left alone", async () => { + expect((await runtimeFlags({ ALTIMATE_CLI_EXPERIMENTAL_OUTPUT_TOKEN_MAX: "4321" })).outputTokenMax).toBe(4321) + expect((await runtimeFlags({ OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX: "1234" })).outputTokenMax).toBe(1234) + // A Config key with no OPENCODE_ prefix goes straight through the wrap. + class Other extends ConfigService.Service()("@test/Other", { + name: Config.string("OTHER_NAME").pipe(Config.withDefault("none")), + }) {} + const other = await Effect.runPromise( + Effect.gen(function* () { + return yield* Other + }).pipe( + Effect.provide( + Other.defaultLayer.pipe( + Layer.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: { OTHER_NAME: "x", ALTIMATE_CLI_OTHER_NAME: "y" } }))), + ), + ), + ), + ) + expect(other.name).toBe("x") + // `fromEnv` answers a prefix path with a Record node: not a value of the flag. + expect((await runtimeFlags({ ALTIMATE_CLI_CLIENT_CHILD: "x", OPENCODE_CLIENT: "vscode" })).client).toBe("vscode") + }) + + test("server auth reads ALTIMATE_CLI_SERVER_PASSWORD / _USERNAME", async () => { + const config = await serverAuth({ ALTIMATE_CLI_SERVER_PASSWORD: "s3cret", ALTIMATE_CLI_SERVER_USERNAME: "kit" }) + expect(Option.getOrUndefined(config.password)).toBe("s3cret") + expect(config.username).toBe("kit") + }) +}) + +// `packages/core`'s `Flag` object is the other import-time reader (31 files in this package, +// `config/config.ts` among them), and the two Effect `Config` flags it carries resolve +// through the ambient provider. Same rule, same subprocess harness. +describe("the core Flag object reads the documented names", () => { + async function coreFlag(name: string, env: Record): Promise { + const script = + `import { Flag } from "@opencode-ai/core/flag/flag"; import { Effect } from "effect";` + + `const v = Flag.${name}; Promise.resolve(Effect.isEffect(v) ? Effect.runPromise(Effect.gen(function* () { return yield* v })) : v)` + + `.then((x) => console.log(JSON.stringify(x ?? null)))` + const proc = Bun.spawn(["bun", "-e", script], { + cwd: path.resolve(import.meta.dir, "../.."), + env: { PATH: process.env.PATH!, HOME: process.env.HOME!, NODE_OPTIONS: "", ...env }, + stdout: "pipe", + stderr: "pipe", + }) + const out = await new Response(proc.stdout).text() + const code = await proc.exited + if (code !== 0) throw new Error(await new Response(proc.stderr).text()) + return JSON.parse(out.trim()) + } + + test("ALTIMATE_CLI_CONFIG / _DIR / _CONTENT reach the core Flag values config.ts reads at import", async () => { + expect(await coreFlag("OPENCODE_CONFIG", { ALTIMATE_CLI_CONFIG: "/documented.json" })).toBe("/documented.json") + expect(await coreFlag("OPENCODE_CONFIG", { ALTIMATE_CLI_CONFIG: "", OPENCODE_CONFIG: "/fallback.json" })).toBe("/fallback.json") + expect(await coreFlag("OPENCODE_CONFIG_DIR", { ALTIMATE_CLI_CONFIG_DIR: "/documented" })).toBe("/documented") + expect(await coreFlag("OPENCODE_CONFIG_CONTENT", { ALTIMATE_CLI_CONFIG_CONTENT: "{}" })).toBe("{}") + }) + + test("a documented false is not overridden by a fallback true", async () => { + expect(await coreFlag("OPENCODE_DISABLE_AUTOUPDATE", { ALTIMATE_CLI_DISABLE_AUTOUPDATE: "false", OPENCODE_DISABLE_AUTOUPDATE: "true" })).toBe(false) + expect(await coreFlag("ALTIMATE_CALM_MODE", { ALTIMATE_CALM_MODE: "false", OPENCODE_CALM_MODE: "true" })).toBe(false) + expect(await flag("OPENCODE_DISABLE_AUTOUPDATE", { ALTIMATE_CLI_DISABLE_AUTOUPDATE: "false", OPENCODE_DISABLE_AUTOUPDATE: "true" })).toBe(false) + }) + + test("the Effect Config flag it carries resolves the documented name, and an invalid documented value does not fall back", async () => { + expect(await coreFlag("OPENCODE_EXPERIMENTAL_FILEWATCHER", { ALTIMATE_CLI_EXPERIMENTAL_FILEWATCHER: "true" })).toBe(true) + expect(await coreFlag("OPENCODE_EXPERIMENTAL_FILEWATCHER", { OPENCODE_EXPERIMENTAL_FILEWATCHER: "true" })).toBe(true) + expect(await coreFlag("OPENCODE_EXPERIMENTAL_FILEWATCHER", {})).toBe(false) + // Set-but-invalid is `false`, as `truthy` says — `Config.orElse`/`Config.option` would + // have silently taken the OPENCODE_ value instead. + expect(await coreFlag("OPENCODE_EXPERIMENTAL_FILEWATCHER", { ALTIMATE_CLI_EXPERIMENTAL_FILEWATCHER: "typo", OPENCODE_EXPERIMENTAL_FILEWATCHER: "true" })).toBe(false) + expect(await coreFlag("OPENCODE_EXPERIMENTAL_FILEWATCHER", { ALTIMATE_CLI_EXPERIMENTAL_FILEWATCHER: "", OPENCODE_EXPERIMENTAL_FILEWATCHER: "true" })).toBe(true) + // The legacy storage path and core read the same database override. + expect(await coreFlag("OPENCODE_DB", { ALTIMATE_CLI_DB: "/tmp/x.db" })).toBe("/tmp/x.db") + expect(await coreFlag("ALTIMATE_CLI_YOLO", { ALTIMATE_CLI_YOLO: "", OPENCODE_YOLO: "true" })).toBe(true) + expect(await flag("ALTIMATE_CLI_YOLO", { ALTIMATE_CLI_YOLO: "", OPENCODE_YOLO: "true" })).toBe(true) + }) +}) + +// coderabbit on #1341: the copy-on-select default is chosen by whether the variable is +// set at all, so a documented `false` must count as set or Windows keeps its default. +test("a documented explicit false overrides the platform default for copy-on-select", async () => { + expect( + await flag("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT", { ALTIMATE_CLI_EXPERIMENTAL_DISABLE_COPY_ON_SELECT: "false" }), + ).toBe(false) + expect( + await flag("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT", { ALTIMATE_CLI_EXPERIMENTAL_DISABLE_COPY_ON_SELECT: "true" }), + ).toBe(true) +})