Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 88 additions & 31 deletions packages/core/src/flag/flag.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,87 @@
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 = {
OTEL_EXPORTER_OTLP_ENDPOINT: process.env["OTEL_EXPORTER_OTLP_ENDPOINT"],
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"),
Expand All @@ -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"),

Expand Down Expand Up @@ -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() {
Expand All @@ -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
Expand Down
9 changes: 5 additions & 4 deletions packages/opencode/src/altimate/tools/project-scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---

Expand Down Expand Up @@ -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<boolean>). 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")
Expand Down
9 changes: 7 additions & 2 deletions packages/opencode/src/cli/cmd/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 }
})()
Expand Down
12 changes: 6 additions & 6 deletions packages/opencode/src/cli/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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

Expand Down
12 changes: 10 additions & 2 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
})
Expand Down
37 changes: 35 additions & 2 deletions packages/opencode/src/effect/config-service.ts
Original file line number Diff line number Diff line change
@@ -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<string, Config.Config<unknown>>

Expand Down Expand Up @@ -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<Fields>)
}),
Expand All @@ -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
Loading
Loading