feat: user config api key (#569)
Adds support for reading OPENAI_API_KEY (and other variables) from a user‑wide dotenv file (~/.codex.config). Precedence order is now: 1. explicit environment variable 2. project‑local .env (loaded earlier) 3. ~/.codex.config Also adds a regression test that ensures the multiline editor correctly handles cases where printable text and the CSI‑u Shift+Enter sequence arrive in the same input chunk. House‑kept with Prettier; removed stray temp.json artifact.
This commit is contained in:
@@ -11,11 +11,37 @@ import type { FullAutoErrorMode } from "./auto-approval-mode.js";
|
|||||||
import { AutoApprovalMode } from "./auto-approval-mode.js";
|
import { AutoApprovalMode } from "./auto-approval-mode.js";
|
||||||
import { log } from "./logger/log.js";
|
import { log } from "./logger/log.js";
|
||||||
import { providers } from "./providers.js";
|
import { providers } from "./providers.js";
|
||||||
|
import { config as loadDotenv } from "dotenv";
|
||||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||||
import { load as loadYaml, dump as dumpYaml } from "js-yaml";
|
import { load as loadYaml, dump as dumpYaml } from "js-yaml";
|
||||||
import { homedir } from "os";
|
import { homedir } from "os";
|
||||||
import { dirname, join, extname, resolve as resolvePath } from "path";
|
import { dirname, join, extname, resolve as resolvePath } from "path";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// User‑wide environment config (~/.codex.env)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Load a user‑level dotenv file **after** process.env and any project‑local
|
||||||
|
// .env file (loaded via "dotenv/config" in cli.tsx) are in place. We rely on
|
||||||
|
// dotenv's default behaviour of *not* overriding existing variables so that
|
||||||
|
// the precedence order becomes:
|
||||||
|
// 1. Explicit environment variables
|
||||||
|
// 2. Project‑local .env (handled in cli.tsx)
|
||||||
|
// 3. User‑wide ~/.codex.env (loaded here)
|
||||||
|
// This guarantees that users can still override the global key on a per‑project
|
||||||
|
// basis while enjoying the convenience of a persistent default.
|
||||||
|
|
||||||
|
// Skip when running inside Vitest to avoid interfering with the FS mocks used
|
||||||
|
// by tests that stub out `fs` *after* importing this module.
|
||||||
|
const USER_WIDE_CONFIG_PATH = join(homedir(), ".codex.env");
|
||||||
|
|
||||||
|
const isVitest =
|
||||||
|
typeof (globalThis as { vitest?: unknown }).vitest !== "undefined";
|
||||||
|
|
||||||
|
if (!isVitest) {
|
||||||
|
loadDotenv({ path: USER_WIDE_CONFIG_PATH });
|
||||||
|
}
|
||||||
|
|
||||||
export const DEFAULT_AGENTIC_MODEL = "o4-mini";
|
export const DEFAULT_AGENTIC_MODEL = "o4-mini";
|
||||||
export const DEFAULT_FULL_CONTEXT_MODEL = "gpt-4.1";
|
export const DEFAULT_FULL_CONTEXT_MODEL = "gpt-4.1";
|
||||||
export const DEFAULT_APPROVAL_MODE = AutoApprovalMode.SUGGEST;
|
export const DEFAULT_APPROVAL_MODE = AutoApprovalMode.SUGGEST;
|
||||||
@@ -117,7 +143,7 @@ export type StoredConfig = {
|
|||||||
// propagating to existing users until they explicitly set a model.
|
// propagating to existing users until they explicitly set a model.
|
||||||
export const EMPTY_STORED_CONFIG: StoredConfig = { model: "" };
|
export const EMPTY_STORED_CONFIG: StoredConfig = { model: "" };
|
||||||
|
|
||||||
// Pre‑stringified JSON variant so we don’t stringify repeatedly.
|
// Pre‑stringified JSON variant so we don't stringify repeatedly.
|
||||||
const EMPTY_CONFIG_JSON = JSON.stringify(EMPTY_STORED_CONFIG, null, 2) + "\n";
|
const EMPTY_CONFIG_JSON = JSON.stringify(EMPTY_STORED_CONFIG, null, 2) + "\n";
|
||||||
|
|
||||||
export type MemoryConfig = {
|
export type MemoryConfig = {
|
||||||
|
|||||||
62
codex-cli/tests/user-config-env.test.ts
Normal file
62
codex-cli/tests/user-config-env.test.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||||
|
import { mkdtempSync, writeFileSync, rmSync } from "fs";
|
||||||
|
import { tmpdir } from "os";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that ~/.codex.env is parsed (lowest‑priority) when present.
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe("user‑wide ~/.codex.env support", () => {
|
||||||
|
const ORIGINAL_HOME = process.env["HOME"];
|
||||||
|
const ORIGINAL_API_KEY = process.env["OPENAI_API_KEY"];
|
||||||
|
|
||||||
|
let tempHome: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Create an isolated fake $HOME directory.
|
||||||
|
tempHome = mkdtempSync(join(tmpdir(), "codex-home-"));
|
||||||
|
process.env["HOME"] = tempHome;
|
||||||
|
|
||||||
|
// Ensure the env var is unset so that the file value is picked up.
|
||||||
|
delete process.env["OPENAI_API_KEY"];
|
||||||
|
|
||||||
|
// Write ~/.codex.env with a dummy key.
|
||||||
|
writeFileSync(
|
||||||
|
join(tempHome, ".codex.env"),
|
||||||
|
"OPENAI_API_KEY=my-home-key\n",
|
||||||
|
{
|
||||||
|
encoding: "utf8",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Cleanup temp directory.
|
||||||
|
try {
|
||||||
|
rmSync(tempHome, { recursive: true, force: true });
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore original env.
|
||||||
|
if (ORIGINAL_HOME !== undefined) {
|
||||||
|
process.env["HOME"] = ORIGINAL_HOME;
|
||||||
|
} else {
|
||||||
|
delete process.env["HOME"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ORIGINAL_API_KEY !== undefined) {
|
||||||
|
process.env["OPENAI_API_KEY"] = ORIGINAL_API_KEY;
|
||||||
|
} else {
|
||||||
|
delete process.env["OPENAI_API_KEY"];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("loads the API key from ~/.codex.env when not set elsewhere", async () => {
|
||||||
|
// Import the config module AFTER setting up the fake env.
|
||||||
|
const { getApiKey } = await import("../src/utils/config.js");
|
||||||
|
|
||||||
|
expect(getApiKey("openai")).toBe("my-home-key");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user