|
1 | | -import { join } from "node:path"; |
| 1 | +import { dirname, join } from "node:path"; |
2 | 2 | import { homedir } from "node:os"; |
3 | 3 | import { rename as fsRename } from "node:fs/promises"; |
4 | 4 |
|
5 | | -const PLUGIN_NAME = "codex-multi-auth"; |
| 5 | +export const PLUGIN_NAME = "codex-multi-auth"; |
| 6 | +export const PLUGIN_MARKETPLACE = "ndycode"; |
| 7 | +export const PLUGIN_VERSION = "local"; |
6 | 8 | export const FILE_RETRY_CODES = new Set(["EBUSY", "EPERM", "EAGAIN", "ENOTEMPTY", "EACCES"]); |
7 | 9 | export const FILE_RETRY_MAX_ATTEMPTS = 6; |
8 | 10 | export const FILE_RETRY_BASE_DELAY_MS = 25; |
9 | 11 | export const FILE_RETRY_JITTER_MS = 20; |
10 | 12 |
|
| 13 | +function firstNonEmpty(values) { |
| 14 | + for (const value of values) { |
| 15 | + const trimmed = (value ?? "").trim(); |
| 16 | + if (trimmed.length > 0) { |
| 17 | + return trimmed; |
| 18 | + } |
| 19 | + } |
| 20 | + return null; |
| 21 | +} |
| 22 | + |
| 23 | +export function resolveCodexHomeDir( |
| 24 | + platform = process.platform, |
| 25 | + env = process.env, |
| 26 | + home = homedir(), |
| 27 | +) { |
| 28 | + const override = (env.CODEX_HOME ?? "").trim(); |
| 29 | + if (override.length > 0) { |
| 30 | + return override; |
| 31 | + } |
| 32 | + if (platform === "win32") { |
| 33 | + const homeDrive = (env.HOMEDRIVE ?? "").trim(); |
| 34 | + const homePath = (env.HOMEPATH ?? "").trim(); |
| 35 | + const drivePathHome = |
| 36 | + homeDrive.length > 0 && homePath.length > 0 |
| 37 | + ? `${homeDrive}${homePath}` |
| 38 | + : undefined; |
| 39 | + return join( |
| 40 | + firstNonEmpty([env.USERPROFILE, env.HOME, drivePathHome, home]) ?? home, |
| 41 | + ".codex", |
| 42 | + ); |
| 43 | + } |
| 44 | + return join(firstNonEmpty([env.HOME, home]) ?? home, ".codex"); |
| 45 | +} |
| 46 | + |
| 47 | +export function makePluginKey( |
| 48 | + pluginName = PLUGIN_NAME, |
| 49 | + marketplaceName = PLUGIN_MARKETPLACE, |
| 50 | +) { |
| 51 | + return `${pluginName}@${marketplaceName}`; |
| 52 | +} |
| 53 | + |
11 | 54 | export function resolveInstallPaths( |
12 | 55 | platform = process.platform, |
13 | 56 | env = process.env, |
14 | 57 | home = homedir(), |
| 58 | + pluginName = PLUGIN_NAME, |
| 59 | + marketplaceName = PLUGIN_MARKETPLACE, |
| 60 | + pluginVersion = PLUGIN_VERSION, |
15 | 61 | ) { |
16 | | - const isWindows = platform === "win32"; |
17 | | - const appData = (env.APPDATA ?? "").trim(); |
18 | | - const localAppData = (env.LOCALAPPDATA ?? appData).trim(); |
19 | | - const configBase = isWindows |
20 | | - ? appData || join(home, "AppData", "Roaming") |
21 | | - : join(home, ".config"); |
22 | | - const cacheBase = isWindows |
23 | | - ? localAppData || join(home, "AppData", "Local") |
24 | | - : join(home, ".cache"); |
25 | | - const configDir = join(configBase, "Codex"); |
26 | | - const configPath = join(configDir, "Codex.json"); |
27 | | - const cacheDir = join(cacheBase, "Codex"); |
| 62 | + const codexHomeDir = resolveCodexHomeDir(platform, env, home); |
| 63 | + const configPathOverride = (env.CODEX_CLI_CONFIG_PATH ?? "").trim(); |
| 64 | + const configPath = configPathOverride.length > 0 |
| 65 | + ? configPathOverride |
| 66 | + : join(codexHomeDir, "config.toml"); |
| 67 | + const configDir = dirname(configPath); |
| 68 | + const pluginsCacheDir = join(codexHomeDir, "plugins", "cache"); |
| 69 | + const pluginBaseDir = join(pluginsCacheDir, marketplaceName, pluginName); |
| 70 | + const pluginInstallDir = join(pluginBaseDir, pluginVersion); |
28 | 71 | return { |
| 72 | + codexHomeDir, |
29 | 73 | configDir, |
30 | 74 | configPath, |
31 | | - cacheDir, |
32 | | - cacheNodeModules: join(cacheDir, "node_modules", PLUGIN_NAME), |
33 | | - cacheBunLock: join(cacheDir, "bun.lock"), |
34 | | - cachePackageJson: join(cacheDir, "package.json"), |
| 75 | + pluginsCacheDir, |
| 76 | + pluginBaseDir, |
| 77 | + pluginInstallDir, |
| 78 | + pluginKey: makePluginKey(pluginName, marketplaceName), |
| 79 | + pluginName, |
| 80 | + marketplaceName, |
| 81 | + pluginVersion, |
35 | 82 | }; |
36 | 83 | } |
37 | 84 |
|
38 | | -export function normalizePluginList(list) { |
39 | | - const entries = Array.isArray(list) ? list.filter(Boolean) : []; |
40 | | - const filtered = entries.filter((entry) => { |
41 | | - if (typeof entry !== "string") return true; |
42 | | - return entry !== PLUGIN_NAME && !entry.startsWith(`${PLUGIN_NAME}@`); |
43 | | - }); |
44 | | - const deduped = []; |
45 | | - const seen = new Set(); |
46 | | - for (const entry of filtered) { |
47 | | - const key = typeof entry === "string" ? `s:${entry}` : `j:${JSON.stringify(entry)}`; |
48 | | - if (seen.has(key)) continue; |
49 | | - seen.add(key); |
50 | | - deduped.push(entry); |
| 85 | +function splitLines(content) { |
| 86 | + return content.replace(/\r\n/g, "\n").split("\n"); |
| 87 | +} |
| 88 | + |
| 89 | +function findSectionRange(lines, sectionHeader) { |
| 90 | + const headerIndex = lines.findIndex((line) => line.trim() === sectionHeader); |
| 91 | + if (headerIndex === -1) { |
| 92 | + return null; |
| 93 | + } |
| 94 | + let endIndex = lines.length; |
| 95 | + for (let index = headerIndex + 1; index < lines.length; index += 1) { |
| 96 | + if (/^\s*\[[^\]]+\]\s*$/.test(lines[index])) { |
| 97 | + endIndex = index; |
| 98 | + break; |
| 99 | + } |
| 100 | + } |
| 101 | + return { headerIndex, endIndex }; |
| 102 | +} |
| 103 | + |
| 104 | +function upsertTomlBoolean(content, sectionHeader, key, enabled) { |
| 105 | + const lines = splitLines(content.trim()); |
| 106 | + const normalized = lines.length === 1 && lines[0] === "" ? [] : lines; |
| 107 | + const keyLine = `${key} = ${enabled ? "true" : "false"}`; |
| 108 | + const range = findSectionRange(normalized, sectionHeader); |
| 109 | + |
| 110 | + if (!range) { |
| 111 | + if (normalized.length > 0 && normalized[normalized.length - 1] !== "") { |
| 112 | + normalized.push(""); |
| 113 | + } |
| 114 | + normalized.push(sectionHeader, keyLine); |
| 115 | + return `${normalized.join("\n")}\n`; |
| 116 | + } |
| 117 | + |
| 118 | + for (let index = range.headerIndex + 1; index < range.endIndex; index += 1) { |
| 119 | + if (new RegExp(`^\\s*${key}\\s*=`).test(normalized[index])) { |
| 120 | + normalized[index] = keyLine; |
| 121 | + return `${normalized.join("\n")}\n`; |
| 122 | + } |
51 | 123 | } |
52 | | - return [...deduped, PLUGIN_NAME]; |
| 124 | + |
| 125 | + normalized.splice(range.endIndex, 0, keyLine); |
| 126 | + return `${normalized.join("\n")}\n`; |
| 127 | +} |
| 128 | + |
| 129 | +export function mergePluginConfigToml(content, pluginKey) { |
| 130 | + const withFeatures = upsertTomlBoolean(content, "[features]", "plugins", true); |
| 131 | + return upsertTomlBoolean(withFeatures, `[plugins."${pluginKey}"]`, "enabled", true); |
53 | 132 | } |
54 | 133 |
|
55 | 134 | function sleep(ms) { |
56 | | - // Keep this helper local so installer scripts remain standalone and do not depend on lib/. |
57 | 135 | return new Promise((resolve) => { |
58 | 136 | setTimeout(resolve, ms); |
59 | 137 | }); |
|
0 commit comments