From 1b374b63058e76d141e3c9549ab5fa24745db0f6 Mon Sep 17 00:00:00 2001 From: Haider Date: Mon, 21 Sep 2026 20:43:26 +0530 Subject: [PATCH 1/5] fix(flag): read every documented ALTIMATE_CLI_* name, not only two of them `docs/docs/usage/cli.md` documents the flags under `ALTIMATE_CLI_*`, but `flag.ts` read that spelling for `YOLO` and `DISABLE_AUTOUPDATE` only. `ALTIMATE_CLI_DISABLE_EXTERNAL_SKILLS=true` therefore did nothing (#1329), and a cross-check found the same for the rest of the table: the `DISABLE_*` family, `EXPERIMENTAL*`, `ENABLE_EXA`, `CONFIG`, `CONFIG_CONTENT`, `CONFIG_DIR`, `GIT_BASH_PATH`, `PERMISSION`, `SERVER_USERNAME`/`PASSWORD`. The helpers now resolve the documented alias once: any `OPENCODE_*` key is read as `ALTIMATE_CLI_*` first and `OPENCODE_*` second, so every flag gets the dual read without per-flag edits, and the seven direct `process.env` reads go through the same helper. The OPENCODE_ spellings keep working. Test: each case loads the module in a subprocess with exactly the variable under test; a table-driven case walks every `ALTIMATE_CLI_` name in the docs table and asserts it reaches its flag (boolean, string and numeric shapes), so a future documented name that no flag reads fails here. The alias was deleted once to confirm the tests catch it. Closes #1329 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012Q51zFUmPg1WwtS5CrGJE6 --- packages/opencode/src/flag/flag.ts | 47 +++++++++--- .../test/flag/external-skills-flag.test.ts | 76 +++++++++++++++++++ 2 files changed, 113 insertions(+), 10 deletions(-) create mode 100644 packages/opencode/test/flag/external-skills-flag.test.ts diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index f20e1d0444..38c6ca27e7 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -1,10 +1,25 @@ +// 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. +function documentedAlias(key: string): string | undefined { + return key.startsWith("OPENCODE_") ? "ALTIMATE_CLI_" + key.slice("OPENCODE_".length) : undefined +} + +function read(key: string): string | undefined { + const alias = documentedAlias(key) + return (alias !== undefined ? process.env[alias] : undefined) ?? process.env[key] +} +// 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" } @@ -40,11 +55,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 +107,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") @@ -104,8 +125,10 @@ export namespace Flag { export declare const OPENCODE_DISABLE_PROJECT_CONFIG: boolean export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"] 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 @@ -148,7 +171,9 @@ export namespace Flag { 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 @@ -182,7 +207,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, 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..c39cae79cb --- /dev/null +++ b/packages/opencode/test/flag/external-skills-flag.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, test } from "bun:test" +import path from "node:path" + +// 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([]) + }) +}) From 3874862f13f77d40af6112ed99d6661903275b97 Mon Sep 17 00:00:00 2001 From: Haider Date: Mon, 21 Sep 2026 22:02:54 +0530 Subject: [PATCH 2/5] fix(flag): apply the documented-name rule on every read path, not only `Flag.*` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first cut aliased only the opencode `Flag` namespace. Skill discovery does not read that: `skill/index.ts` takes `RuntimeFlags.disableExternalSkills`, an Effect `Config` resolved through the ambient ConfigProvider, and `config.ts` reads `packages/core`'s `Flag` object — so `ALTIMATE_CLI_DISABLE_EXTERNAL_SKILLS` still scanned external skills and `ALTIMATE_CLI_CONFIG` still did nothing. (cubic on #1341) - One definition of the rule, in core `flag/flag.ts`: `documentedAlias`, `env` (documented name first, empty counts as unset), `truthy`/`numberEnv` and every direct `process.env["OPENCODE_*"]` read go through it; the two Effect `Config` flags it carries resolve the documented name via `Config.orElse` - `effect/config-service.ts` wraps whatever ConfigProvider is active so `RuntimeFlags` and `ServerAuthConfig` try the documented spelling first — built with `ConfigProvider.make`, since `orElse`'s fallback bypasses `mapInput` - opencode `flag.ts` imports the rule instead of its own copy; `altTruthy` / `altEnv` let a set documented value win outright, so a documented `false` is not overridden by a fallback `true` - `config.ts` (`OPENCODE_CONFIG_CONTENT`) and `run --attach` (`OPENCODE_SERVER_PASSWORD` / `_USERNAME`) read through `env` instead of `process.env` directly - Tests: RuntimeFlags and ServerAuthConfig through a `fromEnv` provider (documented alone, documented `false` vs fallback `true`, empty documented, numeric); the core `Flag` object in a subprocess (config paths, `altTruthy` precedence, the Effect `Config` flag) Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012Q51zFUmPg1WwtS5CrGJE6 --- packages/core/src/flag/flag.ts | 105 +++++++++++++----- packages/opencode/src/cli/cmd/run.ts | 9 +- packages/opencode/src/config/config.ts | 12 +- .../opencode/src/effect/config-service.ts | 34 +++++- packages/opencode/src/flag/flag.ts | 21 ++-- .../test/flag/external-skills-flag.test.ts | 95 ++++++++++++++++ 6 files changed, 230 insertions(+), 46 deletions(-) diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts index 2b6eb4a8d1..fcef79353e 100644 --- a/packages/core/src/flag/flag.ts +++ b/packages/core/src/flag/flag.ts @@ -1,29 +1,66 @@ import { Config } 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`. */ +function bool(key: string) { + const alias = documentedAlias(key) + const fallback = Config.boolean(key).pipe(Config.withDefault(false)) + return alias === undefined + ? fallback + : Config.boolean(alias).pipe(Config.orElse(() => fallback)) +} +// 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 +68,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 +81,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 +137,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() { @@ -136,7 +183,7 @@ export const Flag = { 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/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/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..6d86df3e55 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,18 @@ 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) + 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 38c6ca27e7..f3a75cdbde 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -3,14 +3,9 @@ // 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. -function documentedAlias(key: string): string | undefined { - return key.startsWith("OPENCODE_") ? "ALTIMATE_CLI_" + key.slice("OPENCODE_".length) : undefined -} - -function read(key: string): string | undefined { - const alias = documentedAlias(key) - return (alias !== undefined ? process.env[alias] : undefined) ?? process.env[key] -} +// 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) { @@ -23,13 +18,17 @@ function falsy(key: string) { 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 diff --git a/packages/opencode/test/flag/external-skills-flag.test.ts b/packages/opencode/test/flag/external-skills-flag.test.ts index c39cae79cb..0168e55417 100644 --- a/packages/opencode/test/flag/external-skills-flag.test.ts +++ b/packages/opencode/test/flag/external-skills-flag.test.ts @@ -1,5 +1,8 @@ import { describe, expect, test } from "bun:test" import path from "node:path" +import { ConfigProvider, Effect, Layer, Option } from "effect" +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 @@ -74,3 +77,95 @@ describe("every ALTIMATE_CLI_ variable the CLI docs table lists reaches its flag 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 and a non-OPENCODE path are unaffected", 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) + }) + + 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 reaches the Flag config.ts reads", 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", 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) + }) +}) From 6413dbe1ab2196d36458fc99aa78a2260fbc858d Mon Sep 17 00:00:00 2001 From: Haider Date: Mon, 21 Sep 2026 22:04:29 +0530 Subject: [PATCH 3/5] fix(flag): route the remaining direct OPENCODE_ reads in the Flag namespace through `read` The copy-on-select default is chosen by whether the variable is set at all, so a documented `false` has to count as set or Windows keeps its default; the other stragglers (models URL/path, fake VCS, the runtime getters) get the same rule for consistency. (coderabbit on #1341) Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012Q51zFUmPg1WwtS5CrGJE6 --- packages/opencode/src/flag/flag.ts | 34 ++++++++++++------- .../test/flag/external-skills-flag.test.ts | 11 ++++++ 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index f3a75cdbde..d47525d060 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -122,7 +122,9 @@ 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 // altimate_change start — documented ALTIMATE_CLI_ name read first (see `read`) export const OPENCODE_SERVER_PASSWORD = read("OPENCODE_SERVER_PASSWORD") @@ -137,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 = @@ -163,8 +167,10 @@ 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") @@ -195,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, @@ -219,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, @@ -234,7 +244,7 @@ Object.defineProperty(Flag, "ALTIMATE_CLI_YOLO", { 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, @@ -255,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, @@ -282,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, @@ -290,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, @@ -305,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/test/flag/external-skills-flag.test.ts b/packages/opencode/test/flag/external-skills-flag.test.ts index 0168e55417..f06c07b170 100644 --- a/packages/opencode/test/flag/external-skills-flag.test.ts +++ b/packages/opencode/test/flag/external-skills-flag.test.ts @@ -169,3 +169,14 @@ describe("the core Flag object reads the documented names", () => { expect(await coreFlag("OPENCODE_EXPERIMENTAL_FILEWATCHER", {})).toBe(false) }) }) + +// 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) +}) From 224e7a5d39682a312b0115209847fc6a5c6baf4e Mon Sep 17 00:00:00 2001 From: Haider Date: Mon, 21 Sep 2026 22:42:08 +0530 Subject: [PATCH 4/5] fix(flag): close the read paths codex found: storage db, upgrade, scan census, provider nodes Codex review of #1341 (gpt-5.6-sol). - `storage/db.ts` read `OPENCODE_DB` directly while core's `Flag.OPENCODE_DB` now aliases `ALTIMATE_CLI_DB`: one process could open two databases. Both read through `env` now - `cli/upgrade.ts` OR-ed the two names, so a documented `false` lost to a fallback `true` on the real update path; `project-scan`'s feature census read raw `OPENCODE_*` and under-reported documented names - Core's Effect `Config` flags used `Config.orElse`, which swallows a parse failure: a set-but-invalid documented value silently took the OPENCODE_ one. The documented value is read as a string and judged by `truthy`'s rule, so invalid is `false` like everywhere else - The provider wrap accepted a Record node (`fromEnv` answers a prefix path with one) as a documented value; only a set, non-empty scalar counts - `ALTIMATE_CLI_YOLO=""` counts as unset in both flag modules - Tests: `config.ts` really loads `ALTIMATE_CLI_CONFIG_CONTENT` (and it wins over the OPENCODE_ one); upgrade precedence; a non-OPENCODE Config key and a prefix-only documented name through the wrap; invalid/empty documented Effect flag; the DB alias; empty YOLO Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012Q51zFUmPg1WwtS5CrGJE6 --- packages/core/src/flag/flag.ts | 22 ++++++++---- .../src/altimate/tools/project-scan.ts | 9 ++--- packages/opencode/src/cli/upgrade.ts | 12 +++---- .../opencode/src/effect/config-service.ts | 5 ++- packages/opencode/src/flag/flag.ts | 2 +- packages/opencode/src/storage/db.ts | 7 +++- .../test/cli/upgrade-decision.test.ts | 5 +++ packages/opencode/test/config/config.test.ts | 31 ++++++++++++++++ .../test/flag/external-skills-flag.test.ts | 35 ++++++++++++++++--- 9 files changed, 105 insertions(+), 23 deletions(-) diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts index fcef79353e..56976af08b 100644 --- a/packages/core/src/flag/flag.ts +++ b/packages/core/src/flag/flag.ts @@ -1,4 +1,4 @@ -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_ @@ -26,13 +26,22 @@ export function truthy(key: string) { } /** 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`. */ + * 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)) - return alias === undefined - ? fallback - : Config.boolean(alias).pipe(Config.orElse(() => fallback)) + 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 @@ -178,8 +187,9 @@ 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" } 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/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/effect/config-service.ts b/packages/opencode/src/effect/config-service.ts index 6d86df3e55..ad341a9276 100644 --- a/packages/opencode/src/effect/config-service.ts +++ b/packages/opencode/src/effect/config-service.ts @@ -89,8 +89,11 @@ export function aliasDocumentedNames(provider: ConfigProvider.ConfigProvider): C 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), + node && node._tag === "Value" && node.value !== "" ? Effect.succeed(node) : provider.load(path), ) }) } diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index d47525d060..a833dedff5 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -240,7 +240,7 @@ 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" } 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 index f06c07b170..03ecaea341 100644 --- a/packages/opencode/test/flag/external-skills-flag.test.ts +++ b/packages/opencode/test/flag/external-skills-flag.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test" import path from "node:path" -import { ConfigProvider, Effect, Layer, Option } from "effect" +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" @@ -117,9 +118,27 @@ describe("the Effect Config-backed flags read the documented names too", () => { expect(empty.client).toBe("vscode") }) - test("a numeric flag and a non-OPENCODE path are unaffected", async () => { + 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_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 () => { @@ -150,7 +169,7 @@ describe("the core Flag object reads the documented names", () => { return JSON.parse(out.trim()) } - test("ALTIMATE_CLI_CONFIG reaches the Flag config.ts reads", async () => { + 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") @@ -163,10 +182,18 @@ describe("the core Flag object reads the documented names", () => { 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", async () => { + 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) }) }) From bbfcb4687fcad0de728cf8a43dc4f3a38c7bab30 Mon Sep 17 00:00:00 2001 From: Haider Date: Mon, 21 Sep 2026 23:28:09 +0530 Subject: [PATCH 5/5] test(flag): make the non-OPENCODE Config case discriminating A mis-scoped wrap would read ALTIMATE_CLI_OTHER_NAME; that is now the env entry set. (bot review) Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012Q51zFUmPg1WwtS5CrGJE6 --- packages/opencode/test/flag/external-skills-flag.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/test/flag/external-skills-flag.test.ts b/packages/opencode/test/flag/external-skills-flag.test.ts index 03ecaea341..f3c427b899 100644 --- a/packages/opencode/test/flag/external-skills-flag.test.ts +++ b/packages/opencode/test/flag/external-skills-flag.test.ts @@ -131,7 +131,7 @@ describe("the Effect Config-backed flags read the documented names too", () => { }).pipe( Effect.provide( Other.defaultLayer.pipe( - Layer.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: { OTHER_NAME: "x", ALTIMATE_CLI_NAME: "y" } }))), + Layer.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: { OTHER_NAME: "x", ALTIMATE_CLI_OTHER_NAME: "y" } }))), ), ), ),