oc-codex-multi-auth 6.0.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +58 -18
  3. package/assets/readme-hero.svg +10 -10
  4. package/config/README.md +1 -1
  5. package/config/minimal-opencode.json +1 -1
  6. package/config/opencode-legacy.json +1 -1
  7. package/config/opencode-modern.json +1 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +98 -2520
  11. package/dist/index.js.map +1 -1
  12. package/dist/lib/accounts/persistence.d.ts +34 -0
  13. package/dist/lib/accounts/persistence.d.ts.map +1 -0
  14. package/dist/lib/accounts/persistence.js +153 -0
  15. package/dist/lib/accounts/persistence.js.map +1 -0
  16. package/dist/lib/accounts/recovery.d.ts +59 -0
  17. package/dist/lib/accounts/recovery.d.ts.map +1 -0
  18. package/dist/lib/accounts/recovery.js +229 -0
  19. package/dist/lib/accounts/recovery.js.map +1 -0
  20. package/dist/lib/accounts/rotation.d.ts +40 -0
  21. package/dist/lib/accounts/rotation.d.ts.map +1 -0
  22. package/dist/lib/accounts/rotation.js +204 -0
  23. package/dist/lib/accounts/rotation.js.map +1 -0
  24. package/dist/lib/accounts/state.d.ts +92 -0
  25. package/dist/lib/accounts/state.d.ts.map +1 -0
  26. package/dist/lib/accounts/state.js +366 -0
  27. package/dist/lib/accounts/state.js.map +1 -0
  28. package/dist/lib/accounts.d.ts +48 -91
  29. package/dist/lib/accounts.d.ts.map +1 -1
  30. package/dist/lib/accounts.js +121 -699
  31. package/dist/lib/accounts.js.map +1 -1
  32. package/dist/lib/auth/auth.d.ts +1 -1
  33. package/dist/lib/auth/auth.d.ts.map +1 -1
  34. package/dist/lib/auth/auth.js +5 -2
  35. package/dist/lib/auth/auth.js.map +1 -1
  36. package/dist/lib/auth/login-runner.d.ts +32 -0
  37. package/dist/lib/auth/login-runner.d.ts.map +1 -1
  38. package/dist/lib/auth/login-runner.js +79 -64
  39. package/dist/lib/auth/login-runner.js.map +1 -1
  40. package/dist/lib/auth/server.d.ts.map +1 -1
  41. package/dist/lib/auth/server.js +3 -8
  42. package/dist/lib/auth/server.js.map +1 -1
  43. package/dist/lib/circuit-breaker.d.ts +53 -2
  44. package/dist/lib/circuit-breaker.d.ts.map +1 -1
  45. package/dist/lib/circuit-breaker.js +65 -6
  46. package/dist/lib/circuit-breaker.js.map +1 -1
  47. package/dist/lib/config.d.ts.map +1 -1
  48. package/dist/lib/config.js +77 -32
  49. package/dist/lib/config.js.map +1 -1
  50. package/dist/lib/error-sentinels.d.ts +20 -0
  51. package/dist/lib/error-sentinels.d.ts.map +1 -0
  52. package/dist/lib/error-sentinels.js +28 -0
  53. package/dist/lib/error-sentinels.js.map +1 -0
  54. package/dist/lib/errors.d.ts +100 -1
  55. package/dist/lib/errors.d.ts.map +1 -1
  56. package/dist/lib/errors.js +116 -1
  57. package/dist/lib/errors.js.map +1 -1
  58. package/dist/lib/logger.d.ts +14 -1
  59. package/dist/lib/logger.d.ts.map +1 -1
  60. package/dist/lib/logger.js +65 -5
  61. package/dist/lib/logger.js.map +1 -1
  62. package/dist/lib/oauth-constants.d.ts +18 -0
  63. package/dist/lib/oauth-constants.d.ts.map +1 -0
  64. package/dist/lib/oauth-constants.js +18 -0
  65. package/dist/lib/oauth-constants.js.map +1 -0
  66. package/dist/lib/oauth-success.d.ts +2 -0
  67. package/dist/lib/oauth-success.d.ts.map +1 -0
  68. package/dist/lib/oauth-success.html +711 -711
  69. package/dist/lib/oauth-success.js +714 -0
  70. package/dist/lib/oauth-success.js.map +1 -0
  71. package/dist/lib/prompts/codex.d.ts.map +1 -1
  72. package/dist/lib/prompts/codex.js +12 -3
  73. package/dist/lib/prompts/codex.js.map +1 -1
  74. package/dist/lib/prompts/opencode-codex.d.ts.map +1 -1
  75. package/dist/lib/prompts/opencode-codex.js +3 -2
  76. package/dist/lib/prompts/opencode-codex.js.map +1 -1
  77. package/dist/lib/recovery/hook.d.ts +43 -0
  78. package/dist/lib/recovery/hook.d.ts.map +1 -0
  79. package/dist/lib/recovery/hook.js +325 -0
  80. package/dist/lib/recovery/hook.js.map +1 -0
  81. package/dist/lib/recovery/index.d.ts +1 -0
  82. package/dist/lib/recovery/index.d.ts.map +1 -1
  83. package/dist/lib/recovery/index.js +1 -0
  84. package/dist/lib/recovery/index.js.map +1 -1
  85. package/dist/lib/recovery/storage.d.ts.map +1 -1
  86. package/dist/lib/recovery/storage.js +5 -1
  87. package/dist/lib/recovery/storage.js.map +1 -1
  88. package/dist/lib/recovery.d.ts +15 -30
  89. package/dist/lib/recovery.d.ts.map +1 -1
  90. package/dist/lib/recovery.js +14 -312
  91. package/dist/lib/recovery.js.map +1 -1
  92. package/dist/lib/request/fetch-helpers.js +1 -1
  93. package/dist/lib/request/fetch-helpers.js.map +1 -1
  94. package/dist/lib/request/response-handler.d.ts.map +1 -1
  95. package/dist/lib/request/response-handler.js +8 -2
  96. package/dist/lib/request/response-handler.js.map +1 -1
  97. package/dist/lib/runtime.d.ts +101 -0
  98. package/dist/lib/runtime.d.ts.map +1 -0
  99. package/dist/lib/runtime.js +77 -0
  100. package/dist/lib/runtime.js.map +1 -0
  101. package/dist/lib/schemas.d.ts +164 -1
  102. package/dist/lib/schemas.d.ts.map +1 -1
  103. package/dist/lib/schemas.js +251 -0
  104. package/dist/lib/schemas.js.map +1 -1
  105. package/dist/lib/storage/atomic-write.d.ts +31 -0
  106. package/dist/lib/storage/atomic-write.d.ts.map +1 -0
  107. package/dist/lib/storage/atomic-write.js +75 -0
  108. package/dist/lib/storage/atomic-write.js.map +1 -0
  109. package/dist/lib/storage/backup.d.ts +13 -0
  110. package/dist/lib/storage/backup.d.ts.map +1 -0
  111. package/dist/lib/storage/backup.js +59 -0
  112. package/dist/lib/storage/backup.js.map +1 -0
  113. package/dist/lib/storage/errors.d.ts +17 -0
  114. package/dist/lib/storage/errors.d.ts.map +1 -0
  115. package/dist/lib/storage/errors.js +38 -0
  116. package/dist/lib/storage/errors.js.map +1 -0
  117. package/dist/lib/storage/export-import.d.ts +84 -0
  118. package/dist/lib/storage/export-import.d.ts.map +1 -0
  119. package/dist/lib/storage/export-import.js +222 -0
  120. package/dist/lib/storage/export-import.js.map +1 -0
  121. package/dist/lib/storage/flagged.d.ts +29 -0
  122. package/dist/lib/storage/flagged.d.ts.map +1 -0
  123. package/dist/lib/storage/flagged.js +207 -0
  124. package/dist/lib/storage/flagged.js.map +1 -0
  125. package/dist/lib/storage/identity.d.ts +72 -0
  126. package/dist/lib/storage/identity.d.ts.map +1 -0
  127. package/dist/lib/storage/identity.js +230 -0
  128. package/dist/lib/storage/identity.js.map +1 -0
  129. package/dist/lib/storage/keychain.d.ts +146 -0
  130. package/dist/lib/storage/keychain.d.ts.map +1 -0
  131. package/dist/lib/storage/keychain.js +278 -0
  132. package/dist/lib/storage/keychain.js.map +1 -0
  133. package/dist/lib/storage/load-save.d.ts +61 -0
  134. package/dist/lib/storage/load-save.d.ts.map +1 -0
  135. package/dist/lib/storage/load-save.js +606 -0
  136. package/dist/lib/storage/load-save.js.map +1 -0
  137. package/dist/lib/storage/migrations.d.ts +35 -0
  138. package/dist/lib/storage/migrations.d.ts.map +1 -1
  139. package/dist/lib/storage/migrations.js +37 -0
  140. package/dist/lib/storage/migrations.js.map +1 -1
  141. package/dist/lib/storage/normalize.d.ts +22 -0
  142. package/dist/lib/storage/normalize.d.ts.map +1 -0
  143. package/dist/lib/storage/normalize.js +114 -0
  144. package/dist/lib/storage/normalize.js.map +1 -0
  145. package/dist/lib/storage/paths.d.ts.map +1 -1
  146. package/dist/lib/storage/paths.js +2 -1
  147. package/dist/lib/storage/paths.js.map +1 -1
  148. package/dist/lib/storage/state.d.ts +46 -0
  149. package/dist/lib/storage/state.d.ts.map +1 -0
  150. package/dist/lib/storage/state.js +100 -0
  151. package/dist/lib/storage/state.js.map +1 -0
  152. package/dist/lib/storage/worktree-lock.d.ts +79 -0
  153. package/dist/lib/storage/worktree-lock.d.ts.map +1 -0
  154. package/dist/lib/storage/worktree-lock.js +265 -0
  155. package/dist/lib/storage/worktree-lock.js.map +1 -0
  156. package/dist/lib/storage.d.ts +21 -155
  157. package/dist/lib/storage.d.ts.map +1 -1
  158. package/dist/lib/storage.js +24 -1167
  159. package/dist/lib/storage.js.map +1 -1
  160. package/dist/lib/tools/_shared.d.ts +16 -0
  161. package/dist/lib/tools/_shared.d.ts.map +1 -0
  162. package/dist/lib/tools/_shared.js +21 -0
  163. package/dist/lib/tools/_shared.js.map +1 -0
  164. package/dist/lib/tools/codex-dashboard.d.ts +8 -0
  165. package/dist/lib/tools/codex-dashboard.d.ts.map +1 -0
  166. package/dist/lib/tools/codex-dashboard.js +176 -0
  167. package/dist/lib/tools/codex-dashboard.js.map +1 -0
  168. package/dist/lib/tools/codex-diag.d.ts +19 -0
  169. package/dist/lib/tools/codex-diag.d.ts.map +1 -0
  170. package/dist/lib/tools/codex-diag.js +139 -0
  171. package/dist/lib/tools/codex-diag.js.map +1 -0
  172. package/dist/lib/tools/codex-diff.d.ts +44 -0
  173. package/dist/lib/tools/codex-diff.d.ts.map +1 -0
  174. package/dist/lib/tools/codex-diff.js +284 -0
  175. package/dist/lib/tools/codex-diff.js.map +1 -0
  176. package/dist/lib/tools/codex-doctor.d.ts +8 -0
  177. package/dist/lib/tools/codex-doctor.d.ts.map +1 -0
  178. package/dist/lib/tools/codex-doctor.js +284 -0
  179. package/dist/lib/tools/codex-doctor.js.map +1 -0
  180. package/dist/lib/tools/codex-export.d.ts +8 -0
  181. package/dist/lib/tools/codex-export.d.ts.map +1 -0
  182. package/dist/lib/tools/codex-export.js +63 -0
  183. package/dist/lib/tools/codex-export.js.map +1 -0
  184. package/dist/lib/tools/codex-health.d.ts +8 -0
  185. package/dist/lib/tools/codex-health.d.ts.map +1 -0
  186. package/dist/lib/tools/codex-health.js +124 -0
  187. package/dist/lib/tools/codex-health.js.map +1 -0
  188. package/dist/lib/tools/codex-help.d.ts +8 -0
  189. package/dist/lib/tools/codex-help.d.ts.map +1 -0
  190. package/dist/lib/tools/codex-help.js +127 -0
  191. package/dist/lib/tools/codex-help.js.map +1 -0
  192. package/dist/lib/tools/codex-import.d.ts +8 -0
  193. package/dist/lib/tools/codex-import.d.ts.map +1 -0
  194. package/dist/lib/tools/codex-import.js +97 -0
  195. package/dist/lib/tools/codex-import.js.map +1 -0
  196. package/dist/lib/tools/codex-keychain.d.ts +36 -0
  197. package/dist/lib/tools/codex-keychain.d.ts.map +1 -0
  198. package/dist/lib/tools/codex-keychain.js +289 -0
  199. package/dist/lib/tools/codex-keychain.js.map +1 -0
  200. package/dist/lib/tools/codex-label.d.ts +8 -0
  201. package/dist/lib/tools/codex-label.d.ts.map +1 -0
  202. package/dist/lib/tools/codex-label.js +143 -0
  203. package/dist/lib/tools/codex-label.js.map +1 -0
  204. package/dist/lib/tools/codex-limits.d.ts +8 -0
  205. package/dist/lib/tools/codex-limits.d.ts.map +1 -0
  206. package/dist/lib/tools/codex-limits.js +497 -0
  207. package/dist/lib/tools/codex-limits.js.map +1 -0
  208. package/dist/lib/tools/codex-list.d.ts +8 -0
  209. package/dist/lib/tools/codex-list.d.ts.map +1 -0
  210. package/dist/lib/tools/codex-list.js +260 -0
  211. package/dist/lib/tools/codex-list.js.map +1 -0
  212. package/dist/lib/tools/codex-metrics.d.ts +8 -0
  213. package/dist/lib/tools/codex-metrics.d.ts.map +1 -0
  214. package/dist/lib/tools/codex-metrics.js +180 -0
  215. package/dist/lib/tools/codex-metrics.js.map +1 -0
  216. package/dist/lib/tools/codex-next.d.ts +8 -0
  217. package/dist/lib/tools/codex-next.d.ts.map +1 -0
  218. package/dist/lib/tools/codex-next.js +54 -0
  219. package/dist/lib/tools/codex-next.js.map +1 -0
  220. package/dist/lib/tools/codex-note.d.ts +8 -0
  221. package/dist/lib/tools/codex-note.d.ts.map +1 -0
  222. package/dist/lib/tools/codex-note.js +79 -0
  223. package/dist/lib/tools/codex-note.js.map +1 -0
  224. package/dist/lib/tools/codex-refresh.d.ts +8 -0
  225. package/dist/lib/tools/codex-refresh.d.ts.map +1 -0
  226. package/dist/lib/tools/codex-refresh.js +78 -0
  227. package/dist/lib/tools/codex-refresh.js.map +1 -0
  228. package/dist/lib/tools/codex-remove.d.ts +8 -0
  229. package/dist/lib/tools/codex-remove.d.ts.map +1 -0
  230. package/dist/lib/tools/codex-remove.js +184 -0
  231. package/dist/lib/tools/codex-remove.js.map +1 -0
  232. package/dist/lib/tools/codex-setup.d.ts +8 -0
  233. package/dist/lib/tools/codex-setup.d.ts.map +1 -0
  234. package/dist/lib/tools/codex-setup.js +26 -0
  235. package/dist/lib/tools/codex-setup.js.map +1 -0
  236. package/dist/lib/tools/codex-status.d.ts +8 -0
  237. package/dist/lib/tools/codex-status.d.ts.map +1 -0
  238. package/dist/lib/tools/codex-status.js +263 -0
  239. package/dist/lib/tools/codex-status.js.map +1 -0
  240. package/dist/lib/tools/codex-switch.d.ts +8 -0
  241. package/dist/lib/tools/codex-switch.d.ts.map +1 -0
  242. package/dist/lib/tools/codex-switch.js +122 -0
  243. package/dist/lib/tools/codex-switch.js.map +1 -0
  244. package/dist/lib/tools/codex-tag.d.ts +8 -0
  245. package/dist/lib/tools/codex-tag.d.ts.map +1 -0
  246. package/dist/lib/tools/codex-tag.js +101 -0
  247. package/dist/lib/tools/codex-tag.js.map +1 -0
  248. package/dist/lib/tools/index.d.ts +143 -0
  249. package/dist/lib/tools/index.d.ts.map +1 -0
  250. package/dist/lib/tools/index.js +67 -0
  251. package/dist/lib/tools/index.js.map +1 -0
  252. package/dist/lib/ui/format.d.ts.map +1 -1
  253. package/dist/lib/ui/format.js +5 -0
  254. package/dist/lib/ui/format.js.map +1 -1
  255. package/dist/lib/ui/runtime.d.ts +10 -0
  256. package/dist/lib/ui/runtime.d.ts.map +1 -1
  257. package/dist/lib/ui/runtime.js +16 -11
  258. package/dist/lib/ui/runtime.js.map +1 -1
  259. package/dist/lib/ui/select.d.ts.map +1 -1
  260. package/dist/lib/ui/select.js +12 -3
  261. package/dist/lib/ui/select.js.map +1 -1
  262. package/dist/lib/ui/theme.d.ts +19 -0
  263. package/dist/lib/ui/theme.d.ts.map +1 -1
  264. package/dist/lib/ui/theme.js +27 -0
  265. package/dist/lib/ui/theme.js.map +1 -1
  266. package/package.json +24 -16
  267. package/scripts/copy-oauth-success.js +16 -6
  268. package/scripts/install-oc-codex-multi-auth-core.js +462 -381
  269. package/scripts/install-oc-codex-multi-auth.js +15 -15
  270. package/scripts/test-all-models.sh +1 -1
@@ -1,381 +1,462 @@
1
- import { existsSync } from "node:fs";
2
- import { copyFile, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
- import { homedir } from "node:os";
4
- import { dirname, join, resolve } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
-
7
- const PACKAGE_NAME = "oc-codex-multi-auth";
8
- const LEGACY_PACKAGE_NAMES = ["oc-chatgpt-multi-auth"];
9
- const WINDOWS_RENAME_RETRY_ATTEMPTS = 5;
10
- const WINDOWS_RENAME_RETRY_BASE_DELAY_MS = 10;
11
-
12
- function getManagedPackageNames() {
13
- return [PACKAGE_NAME, ...LEGACY_PACKAGE_NAMES];
14
- }
15
-
16
- function printHelp() {
17
- console.log(`Usage: ${PACKAGE_NAME} [--modern|--legacy] [--dry-run] [--no-cache-clear]\n\n` +
18
- "Default behavior:\n" +
19
- " - Installs/updates global config at ~/.config/opencode/opencode.json\n" +
20
- " - Uses full catalog config by default (9 base models + 34 explicit presets)\n" +
21
- " - Ensures plugin is unpinned (latest)\n" +
22
- " - Clears OpenCode plugin cache\n\n" +
23
- "Options:\n" +
24
- " --modern Force compact modern config (9 base models + --variant presets)\n" +
25
- " --legacy Force explicit legacy config (34 preset model entries)\n" +
26
- " --dry-run Show actions without writing\n" +
27
- " --no-cache-clear Skip clearing OpenCode cache\n"
28
- );
29
- }
30
-
31
- const scriptDir = dirname(fileURLToPath(import.meta.url));
32
- const repoRoot = resolve(scriptDir, "..");
33
- const modernTemplatePath = join(repoRoot, "config", "opencode-modern.json");
34
- const legacyTemplatePath = join(repoRoot, "config", "opencode-legacy.json");
35
-
36
- function log(message) {
37
- console.log(message);
38
- }
39
-
40
- function delay(ms) {
41
- return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
42
- }
43
-
44
- function isWindowsLockError(error) {
45
- const code = error?.code;
46
- return code === "EPERM" || code === "EBUSY";
47
- }
48
-
49
- function formatErrorForLog(error) {
50
- if (error instanceof Error) {
51
- return error.message;
52
- }
53
- return String(error);
54
- }
55
-
56
- function resolveHomeDirectory(env = process.env) {
57
- return env.HOME || env.USERPROFILE || homedir();
58
- }
59
-
60
- function buildPaths(homeDir) {
61
- const configDir = join(homeDir, ".config", "opencode");
62
- const cacheDir = join(homeDir, ".cache", "opencode");
63
- return {
64
- configDir,
65
- configPath: join(configDir, "opencode.json"),
66
- cacheDir,
67
- cacheNodeModulesPaths: getManagedPackageNames().map((name) => join(cacheDir, "node_modules", name)),
68
- cacheBunLock: join(cacheDir, "bun.lock"),
69
- cachePackageJson: join(cacheDir, "package.json"),
70
- modernTemplatePath,
71
- legacyTemplatePath,
72
- };
73
- }
74
-
75
- function parseCliArgs(argv = process.argv.slice(2)) {
76
- const args = new Set(argv);
77
- if (args.has("--help") || args.has("-h")) {
78
- return {
79
- wantsHelp: true,
80
- };
81
- }
82
-
83
- const requestedModern = args.has("--modern");
84
- const requestedLegacy = args.has("--legacy");
85
-
86
- if (requestedModern && requestedLegacy) {
87
- throw new Error("Choose only one of --modern or --legacy.");
88
- }
89
-
90
- return {
91
- wantsHelp: false,
92
- dryRun: args.has("--dry-run"),
93
- skipCacheClear: args.has("--no-cache-clear"),
94
- configMode: requestedModern ? "modern" : requestedLegacy ? "legacy" : "full",
95
- };
96
- }
97
-
98
- function normalizePluginList(list) {
99
- const entries = Array.isArray(list) ? list.filter(Boolean) : [];
100
- const managedNames = getManagedPackageNames();
101
- const filtered = entries.filter((entry) => {
102
- if (typeof entry !== "string") return true;
103
- return !managedNames.some((name) => entry === name || entry.startsWith(`${name}@`));
104
- });
105
- return [...filtered, PACKAGE_NAME];
106
- }
107
-
108
- function formatJson(obj) {
109
- return `${JSON.stringify(obj, null, 2)}\n`;
110
- }
111
-
112
- function mergeFullTemplate(modernTemplate, legacyTemplate) {
113
- const modernModels = modernTemplate.provider?.openai?.models ?? {};
114
- const legacyModels = legacyTemplate.provider?.openai?.models ?? {};
115
- const overlappingKeys = Object.keys(modernModels).filter((key) => Object.hasOwn(legacyModels, key));
116
-
117
- if (overlappingKeys.length > 0) {
118
- throw new Error(`Full config template collision for model keys: ${overlappingKeys.join(", ")}`);
119
- }
120
-
121
- return {
122
- ...modernTemplate,
123
- provider: {
124
- ...(modernTemplate.provider ?? {}),
125
- openai: {
126
- ...(modernTemplate.provider?.openai ?? {}),
127
- models: {
128
- ...modernModels,
129
- ...legacyModels,
130
- },
131
- },
132
- },
133
- };
134
- }
135
-
136
- async function readJson(filePath) {
137
- const content = await readFile(filePath, "utf-8");
138
- return JSON.parse(content);
139
- }
140
-
141
- async function renameWithWindowsRetry(sourcePath, destinationPath) {
142
- let lastError = null;
143
-
144
- for (let attempt = 0; attempt < WINDOWS_RENAME_RETRY_ATTEMPTS; attempt += 1) {
145
- try {
146
- await rename(sourcePath, destinationPath);
147
- return;
148
- } catch (error) {
149
- if (isWindowsLockError(error)) {
150
- lastError = error;
151
- await delay(WINDOWS_RENAME_RETRY_BASE_DELAY_MS * 2 ** attempt);
152
- continue;
153
- }
154
- throw error;
155
- }
156
- }
157
-
158
- if (lastError) {
159
- throw lastError;
160
- }
161
- }
162
-
163
- async function writeFileAtomic(filePath, content) {
164
- const uniqueSuffix = `${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
165
- const tempPath = `${filePath}.${uniqueSuffix}.tmp`;
166
-
167
- try {
168
- await mkdir(dirname(filePath), { recursive: true });
169
- await writeFile(tempPath, content, { encoding: "utf-8", mode: 0o600 });
170
- await renameWithWindowsRetry(tempPath, filePath);
171
- } catch (error) {
172
- await rm(tempPath, { force: true }).catch(() => {});
173
- throw error;
174
- }
175
- }
176
-
177
- async function loadTemplate(mode, paths) {
178
- if (mode === "modern") {
179
- return readJson(paths.modernTemplatePath);
180
- }
181
- if (mode === "legacy") {
182
- return readJson(paths.legacyTemplatePath);
183
- }
184
-
185
- const [modernTemplate, legacyTemplate] = await Promise.all([
186
- readJson(paths.modernTemplatePath),
187
- readJson(paths.legacyTemplatePath),
188
- ]);
189
-
190
- return mergeFullTemplate(modernTemplate, legacyTemplate);
191
- }
192
-
193
- async function copyFileWithWindowsRetry(sourcePath, destinationPath) {
194
- let lastError = null;
195
-
196
- for (let attempt = 0; attempt < WINDOWS_RENAME_RETRY_ATTEMPTS; attempt += 1) {
197
- try {
198
- await copyFile(sourcePath, destinationPath);
199
- return;
200
- } catch (error) {
201
- if (isWindowsLockError(error)) {
202
- lastError = error;
203
- await delay(WINDOWS_RENAME_RETRY_BASE_DELAY_MS * 2 ** attempt);
204
- continue;
205
- }
206
- throw error;
207
- }
208
- }
209
-
210
- if (lastError) {
211
- throw lastError;
212
- }
213
- }
214
-
215
- async function backupConfig(sourcePath, dryRun) {
216
- const timestamp = new Date()
217
- .toISOString()
218
- .replace(/[:.]/g, "-")
219
- .replace("T", "_")
220
- .replace("Z", "");
221
- const backupPath = `${sourcePath}.bak-${timestamp}`;
222
- if (!dryRun) {
223
- await copyFileWithWindowsRetry(sourcePath, backupPath);
224
- }
225
- return backupPath;
226
- }
227
-
228
- async function removePluginFromCachePackage(paths, dryRun) {
229
- if (!existsSync(paths.cachePackageJson)) {
230
- return;
231
- }
232
-
233
- let cacheData;
234
- try {
235
- cacheData = await readJson(paths.cachePackageJson);
236
- } catch (error) {
237
- log(`Warning: Could not parse ${paths.cachePackageJson} (${formatErrorForLog(error)}). Skipping.`);
238
- return;
239
- }
240
-
241
- const sections = [
242
- "dependencies",
243
- "devDependencies",
244
- "peerDependencies",
245
- "optionalDependencies",
246
- ];
247
-
248
- let changed = false;
249
- for (const section of sections) {
250
- const deps = cacheData?.[section];
251
- if (deps && typeof deps === "object") {
252
- for (const name of getManagedPackageNames()) {
253
- if (name in deps) {
254
- delete deps[name];
255
- changed = true;
256
- }
257
- }
258
- }
259
- }
260
-
261
- if (!changed) {
262
- return;
263
- }
264
-
265
- if (dryRun) {
266
- log(`[dry-run] Would update ${paths.cachePackageJson} to remove ${getManagedPackageNames().join(", ")}`);
267
- return;
268
- }
269
-
270
- await writeFileAtomic(paths.cachePackageJson, formatJson(cacheData));
271
- }
272
-
273
- async function clearCache(paths, dryRun, skipCacheClear) {
274
- if (skipCacheClear) {
275
- log("Skipping cache clear (--no-cache-clear).");
276
- await removePluginFromCachePackage(paths, dryRun);
277
- return;
278
- }
279
-
280
- if (dryRun) {
281
- for (const cacheNodeModulesPath of paths.cacheNodeModulesPaths) {
282
- log(`[dry-run] Would remove ${cacheNodeModulesPath}`);
283
- }
284
- log(`[dry-run] Would remove ${paths.cacheBunLock}`);
285
- } else {
286
- for (const cacheNodeModulesPath of paths.cacheNodeModulesPaths) {
287
- await rm(cacheNodeModulesPath, { recursive: true, force: true });
288
- }
289
- await rm(paths.cacheBunLock, { force: true });
290
- }
291
-
292
- await removePluginFromCachePackage(paths, dryRun);
293
- }
294
-
295
- export async function runInstaller(argv = process.argv.slice(2), options = {}) {
296
- const parsed = parseCliArgs(argv);
297
- if (parsed.wantsHelp) {
298
- printHelp();
299
- return { exitCode: 0, action: "help" };
300
- }
301
-
302
- const { env = process.env } = options;
303
- const { configMode, dryRun, skipCacheClear } = parsed;
304
- const paths = buildPaths(resolveHomeDirectory(env));
305
- const requiredTemplatePaths = configMode === "modern"
306
- ? [paths.modernTemplatePath]
307
- : configMode === "legacy"
308
- ? [paths.legacyTemplatePath]
309
- : [paths.modernTemplatePath, paths.legacyTemplatePath];
310
-
311
- for (const templatePath of requiredTemplatePaths) {
312
- if (!existsSync(templatePath)) {
313
- throw new Error(`Config template not found at ${templatePath}`);
314
- }
315
- }
316
-
317
- const template = await loadTemplate(configMode, paths);
318
- template.plugin = [PACKAGE_NAME];
319
-
320
- let nextConfig = template;
321
- if (existsSync(paths.configPath)) {
322
- const backupPath = await backupConfig(paths.configPath, dryRun);
323
- log(`${dryRun ? "[dry-run] Would create backup" : "Backup created"}: ${backupPath}`);
324
-
325
- try {
326
- const existing = await readJson(paths.configPath);
327
- const merged = { ...existing };
328
- merged.plugin = normalizePluginList(existing.plugin);
329
- const provider = (existing.provider && typeof existing.provider === "object")
330
- ? { ...existing.provider }
331
- : {};
332
- provider.openai = template.provider.openai;
333
- merged.provider = provider;
334
- nextConfig = merged;
335
- } catch (error) {
336
- log(`Warning: Could not parse existing config (${formatErrorForLog(error)}). Replacing with template.`);
337
- nextConfig = template;
338
- }
339
- } else {
340
- log("No existing config found. Creating new global config.");
341
- }
342
-
343
- if (dryRun) {
344
- log(`[dry-run] Would write ${paths.configPath} using ${configMode} config`);
345
- } else {
346
- await writeFileAtomic(paths.configPath, formatJson(nextConfig));
347
- log(`Wrote ${paths.configPath} (${configMode} config)`);
348
- }
349
-
350
- await clearCache(paths, dryRun, skipCacheClear);
351
-
352
- log("\nDone. Restart OpenCode to (re)install the plugin.");
353
- log("Example: opencode");
354
- if (configMode === "modern") {
355
- log("Note: Modern config intentionally shows 9 base model entries; use --variant to access all 34 shipped presets.");
356
- }
357
- if (configMode === "legacy") {
358
- log("Note: Legacy config writes 34 explicit preset entries and is also safe for older OpenCode versions.");
359
- }
360
- if (configMode === "full") {
361
- log("Note: Full config installs both modern base models and explicit preset entries so the full shipped catalog is visible by default.");
362
- }
363
-
364
- return {
365
- exitCode: 0,
366
- action: "install",
367
- configMode,
368
- configPath: paths.configPath,
369
- };
370
- }
371
-
372
- export const __test = {
373
- buildPaths,
374
- backupConfig,
375
- copyFileWithWindowsRetry,
376
- mergeFullTemplate,
377
- parseCliArgs,
378
- writeFileAtomic,
379
- renameWithWindowsRetry,
380
- resolveHomeDirectory,
381
- };
1
+ import { existsSync } from "node:fs";
2
+ import { copyFile, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const PACKAGE_NAME = "oc-codex-multi-auth";
8
+ const LEGACY_PACKAGE_NAMES = ["oc-chatgpt-multi-auth"];
9
+ const WINDOWS_RENAME_RETRY_ATTEMPTS = 5;
10
+ const WINDOWS_RENAME_RETRY_BASE_DELAY_MS = 10;
11
+
12
+ function getManagedPackageNames() {
13
+ return [PACKAGE_NAME, ...LEGACY_PACKAGE_NAMES];
14
+ }
15
+
16
+ function printHelp() {
17
+ console.log(`Usage: ${PACKAGE_NAME} [--modern|--legacy] [--dry-run] [--no-cache-clear]\n\n` +
18
+ "Default behavior:\n" +
19
+ " - Installs/updates global config at ~/.config/opencode/opencode.json\n" +
20
+ " - Uses full catalog config by default (9 base models + 34 explicit presets)\n" +
21
+ " - Ensures plugin is unpinned (latest)\n" +
22
+ " - Clears OpenCode plugin cache\n\n" +
23
+ "Options:\n" +
24
+ " --modern Force compact modern config (9 base models + --variant presets)\n" +
25
+ " --legacy Force explicit legacy config (34 preset model entries)\n" +
26
+ " --dry-run Show actions without writing\n" +
27
+ " --no-cache-clear Skip clearing OpenCode cache\n"
28
+ );
29
+ }
30
+
31
+ const scriptDir = dirname(fileURLToPath(import.meta.url));
32
+ const repoRoot = resolve(scriptDir, "..");
33
+ const modernTemplatePath = join(repoRoot, "config", "opencode-modern.json");
34
+ const legacyTemplatePath = join(repoRoot, "config", "opencode-legacy.json");
35
+
36
+ function log(message) {
37
+ console.log(message);
38
+ }
39
+
40
+ function delay(ms) {
41
+ return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
42
+ }
43
+
44
+ function isWindowsLockError(error) {
45
+ const code = error?.code;
46
+ return code === "EPERM" || code === "EBUSY";
47
+ }
48
+
49
+ function formatErrorForLog(error) {
50
+ if (error instanceof Error) {
51
+ return error.message;
52
+ }
53
+ return String(error);
54
+ }
55
+
56
+ function resolveHomeDirectory(env = process.env) {
57
+ return env.HOME || env.USERPROFILE || homedir();
58
+ }
59
+
60
+ function buildPaths(homeDir) {
61
+ const configDir = join(homeDir, ".config", "opencode");
62
+ const cacheDir = join(homeDir, ".cache", "opencode");
63
+ return {
64
+ configDir,
65
+ configPath: join(configDir, "opencode.json"),
66
+ cacheDir,
67
+ cacheNodeModulesPaths: getManagedPackageNames().map((name) => join(cacheDir, "node_modules", name)),
68
+ cacheBunLock: join(cacheDir, "bun.lock"),
69
+ cachePackageJson: join(cacheDir, "package.json"),
70
+ modernTemplatePath,
71
+ legacyTemplatePath,
72
+ };
73
+ }
74
+
75
+ function parseCliArgs(argv = process.argv.slice(2)) {
76
+ const args = new Set(argv);
77
+ if (args.has("--help") || args.has("-h")) {
78
+ return {
79
+ wantsHelp: true,
80
+ };
81
+ }
82
+
83
+ const requestedModern = args.has("--modern");
84
+ const requestedLegacy = args.has("--legacy");
85
+
86
+ if (requestedModern && requestedLegacy) {
87
+ throw new Error("Choose only one of --modern or --legacy.");
88
+ }
89
+
90
+ return {
91
+ wantsHelp: false,
92
+ dryRun: args.has("--dry-run"),
93
+ skipCacheClear: args.has("--no-cache-clear"),
94
+ configMode: requestedModern ? "modern" : requestedLegacy ? "legacy" : "full",
95
+ };
96
+ }
97
+
98
+ function normalizePluginList(list) {
99
+ const entries = Array.isArray(list) ? list.filter(Boolean) : [];
100
+ const managedNames = getManagedPackageNames();
101
+ const filtered = entries.filter((entry) => {
102
+ if (typeof entry !== "string") return true;
103
+ return !managedNames.some((name) => entry === name || entry.startsWith(`${name}@`));
104
+ });
105
+ return [...filtered, PACKAGE_NAME];
106
+ }
107
+
108
+ function formatJson(obj) {
109
+ return `${JSON.stringify(obj, null, 2)}\n`;
110
+ }
111
+
112
+ // Top-level keys inside `provider.openai` that the installer owns absolutely.
113
+ // These are always sourced from the template (overwritten or removed) so the
114
+ // plugin's required runtime shape is authoritative. Any OTHER key the user has
115
+ // placed under `provider.openai` is preserved as-is. `models` is handled
116
+ // separately because it's a map where user-added model ids must survive while
117
+ // template-shipped ids win on collision.
118
+ const MANAGED_OPENAI_KEYS = new Set(["baseURL", "apiKey", "options"]);
119
+
120
+ function isPlainObject(value) {
121
+ return value !== null && typeof value === "object" && !Array.isArray(value);
122
+ }
123
+
124
+ // Deep-merge `provider.openai` preserving unknown user keys while letting the
125
+ // installer overwrite the managed shape it ships. This replaces the earlier
126
+ // wholesale overwrite which clobbered custom user-added keys (see audit top-20
127
+ // #6).
128
+ function mergeOpenaiProvider(existingOpenai, templateOpenai) {
129
+ const existingSafe = isPlainObject(existingOpenai) ? existingOpenai : {};
130
+ const templateSafe = isPlainObject(templateOpenai) ? templateOpenai : {};
131
+
132
+ const result = {};
133
+
134
+ // 1. Start with the user's non-managed keys (unknown-to-installer settings).
135
+ for (const [key, value] of Object.entries(existingSafe)) {
136
+ if (MANAGED_OPENAI_KEYS.has(key)) continue;
137
+ if (key === "models") continue; // handled explicitly below
138
+ result[key] = value;
139
+ }
140
+
141
+ // 2. Apply template-managed keys. Installer is source of truth for these.
142
+ for (const [key, value] of Object.entries(templateSafe)) {
143
+ if (key === "models") continue; // handled explicitly below
144
+ result[key] = value;
145
+ }
146
+
147
+ // 3. Merge `models` by id: template wins on collision, user-added ids survive.
148
+ const existingModels = isPlainObject(existingSafe.models) ? existingSafe.models : {};
149
+ const templateModels = isPlainObject(templateSafe.models) ? templateSafe.models : {};
150
+ const mergedModels = { ...existingModels, ...templateModels };
151
+ if (Object.keys(mergedModels).length > 0) {
152
+ result.models = mergedModels;
153
+ }
154
+
155
+ return result;
156
+ }
157
+
158
+ // Naive line-by-line diff for displaying config changes in dry-run. Good enough
159
+ // for eyeballing; not intended to be parsed or round-tripped.
160
+ function formatConfigDiff(existingConfig, nextConfig) {
161
+ const oldText = existingConfig === undefined ? "" : formatJson(existingConfig);
162
+ const newText = formatJson(nextConfig);
163
+ if (oldText === newText) {
164
+ return "(no changes)";
165
+ }
166
+ const lines = [];
167
+ lines.push("--- existing");
168
+ lines.push("+++ proposed");
169
+ if (existingConfig === undefined) {
170
+ lines.push("- (no existing config)");
171
+ } else {
172
+ for (const line of oldText.split("\n")) {
173
+ lines.push(`- ${line}`);
174
+ }
175
+ }
176
+ for (const line of newText.split("\n")) {
177
+ lines.push(`+ ${line}`);
178
+ }
179
+ return lines.join("\n");
180
+ }
181
+
182
+ function mergeFullTemplate(modernTemplate, legacyTemplate) {
183
+ const modernModels = modernTemplate.provider?.openai?.models ?? {};
184
+ const legacyModels = legacyTemplate.provider?.openai?.models ?? {};
185
+ const overlappingKeys = Object.keys(modernModels).filter((key) => Object.hasOwn(legacyModels, key));
186
+
187
+ if (overlappingKeys.length > 0) {
188
+ throw new Error(`Full config template collision for model keys: ${overlappingKeys.join(", ")}`);
189
+ }
190
+
191
+ return {
192
+ ...modernTemplate,
193
+ provider: {
194
+ ...(modernTemplate.provider ?? {}),
195
+ openai: {
196
+ ...(modernTemplate.provider?.openai ?? {}),
197
+ models: {
198
+ ...modernModels,
199
+ ...legacyModels,
200
+ },
201
+ },
202
+ },
203
+ };
204
+ }
205
+
206
+ async function readJson(filePath) {
207
+ const content = await readFile(filePath, "utf-8");
208
+ return JSON.parse(content);
209
+ }
210
+
211
+ async function renameWithWindowsRetry(sourcePath, destinationPath) {
212
+ let lastError = null;
213
+
214
+ for (let attempt = 0; attempt < WINDOWS_RENAME_RETRY_ATTEMPTS; attempt += 1) {
215
+ try {
216
+ await rename(sourcePath, destinationPath);
217
+ return;
218
+ } catch (error) {
219
+ if (isWindowsLockError(error)) {
220
+ lastError = error;
221
+ await delay(WINDOWS_RENAME_RETRY_BASE_DELAY_MS * 2 ** attempt);
222
+ continue;
223
+ }
224
+ throw error;
225
+ }
226
+ }
227
+
228
+ if (lastError) {
229
+ throw lastError;
230
+ }
231
+ }
232
+
233
+ async function writeFileAtomic(filePath, content) {
234
+ const uniqueSuffix = `${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
235
+ const tempPath = `${filePath}.${uniqueSuffix}.tmp`;
236
+
237
+ try {
238
+ await mkdir(dirname(filePath), { recursive: true });
239
+ await writeFile(tempPath, content, { encoding: "utf-8", mode: 0o600 });
240
+ await renameWithWindowsRetry(tempPath, filePath);
241
+ } catch (error) {
242
+ await rm(tempPath, { force: true }).catch(() => {});
243
+ throw error;
244
+ }
245
+ }
246
+
247
+ async function loadTemplate(mode, paths) {
248
+ if (mode === "modern") {
249
+ return readJson(paths.modernTemplatePath);
250
+ }
251
+ if (mode === "legacy") {
252
+ return readJson(paths.legacyTemplatePath);
253
+ }
254
+
255
+ const [modernTemplate, legacyTemplate] = await Promise.all([
256
+ readJson(paths.modernTemplatePath),
257
+ readJson(paths.legacyTemplatePath),
258
+ ]);
259
+
260
+ return mergeFullTemplate(modernTemplate, legacyTemplate);
261
+ }
262
+
263
+ async function copyFileWithWindowsRetry(sourcePath, destinationPath) {
264
+ let lastError = null;
265
+
266
+ for (let attempt = 0; attempt < WINDOWS_RENAME_RETRY_ATTEMPTS; attempt += 1) {
267
+ try {
268
+ await copyFile(sourcePath, destinationPath);
269
+ return;
270
+ } catch (error) {
271
+ if (isWindowsLockError(error)) {
272
+ lastError = error;
273
+ await delay(WINDOWS_RENAME_RETRY_BASE_DELAY_MS * 2 ** attempt);
274
+ continue;
275
+ }
276
+ throw error;
277
+ }
278
+ }
279
+
280
+ if (lastError) {
281
+ throw lastError;
282
+ }
283
+ }
284
+
285
+ async function backupConfig(sourcePath, dryRun) {
286
+ const timestamp = new Date()
287
+ .toISOString()
288
+ .replace(/[:.]/g, "-")
289
+ .replace("T", "_")
290
+ .replace("Z", "");
291
+ const backupPath = `${sourcePath}.bak-${timestamp}`;
292
+ if (!dryRun) {
293
+ await copyFileWithWindowsRetry(sourcePath, backupPath);
294
+ }
295
+ return backupPath;
296
+ }
297
+
298
+ async function removePluginFromCachePackage(paths, dryRun) {
299
+ if (!existsSync(paths.cachePackageJson)) {
300
+ return;
301
+ }
302
+
303
+ let cacheData;
304
+ try {
305
+ cacheData = await readJson(paths.cachePackageJson);
306
+ } catch (error) {
307
+ log(`Warning: Could not parse ${paths.cachePackageJson} (${formatErrorForLog(error)}). Skipping.`);
308
+ return;
309
+ }
310
+
311
+ const sections = [
312
+ "dependencies",
313
+ "devDependencies",
314
+ "peerDependencies",
315
+ "optionalDependencies",
316
+ ];
317
+
318
+ let changed = false;
319
+ for (const section of sections) {
320
+ const deps = cacheData?.[section];
321
+ if (deps && typeof deps === "object") {
322
+ for (const name of getManagedPackageNames()) {
323
+ if (name in deps) {
324
+ delete deps[name];
325
+ changed = true;
326
+ }
327
+ }
328
+ }
329
+ }
330
+
331
+ if (!changed) {
332
+ return;
333
+ }
334
+
335
+ if (dryRun) {
336
+ log(`[dry-run] Would update ${paths.cachePackageJson} to remove ${getManagedPackageNames().join(", ")}`);
337
+ return;
338
+ }
339
+
340
+ await writeFileAtomic(paths.cachePackageJson, formatJson(cacheData));
341
+ }
342
+
343
+ async function clearCache(paths, dryRun, skipCacheClear) {
344
+ if (skipCacheClear) {
345
+ log("Skipping cache clear (--no-cache-clear).");
346
+ await removePluginFromCachePackage(paths, dryRun);
347
+ return;
348
+ }
349
+
350
+ if (dryRun) {
351
+ for (const cacheNodeModulesPath of paths.cacheNodeModulesPaths) {
352
+ log(`[dry-run] Would remove ${cacheNodeModulesPath}`);
353
+ }
354
+ log(`[dry-run] Would remove ${paths.cacheBunLock}`);
355
+ } else {
356
+ for (const cacheNodeModulesPath of paths.cacheNodeModulesPaths) {
357
+ await rm(cacheNodeModulesPath, { recursive: true, force: true });
358
+ }
359
+ await rm(paths.cacheBunLock, { force: true });
360
+ }
361
+
362
+ await removePluginFromCachePackage(paths, dryRun);
363
+ }
364
+
365
+ export async function runInstaller(argv = process.argv.slice(2), options = {}) {
366
+ const parsed = parseCliArgs(argv);
367
+ if (parsed.wantsHelp) {
368
+ printHelp();
369
+ return { exitCode: 0, action: "help" };
370
+ }
371
+
372
+ const { env = process.env } = options;
373
+ const { configMode, dryRun, skipCacheClear } = parsed;
374
+ const paths = buildPaths(resolveHomeDirectory(env));
375
+ const requiredTemplatePaths = configMode === "modern"
376
+ ? [paths.modernTemplatePath]
377
+ : configMode === "legacy"
378
+ ? [paths.legacyTemplatePath]
379
+ : [paths.modernTemplatePath, paths.legacyTemplatePath];
380
+
381
+ for (const templatePath of requiredTemplatePaths) {
382
+ if (!existsSync(templatePath)) {
383
+ throw new Error(`Config template not found at ${templatePath}`);
384
+ }
385
+ }
386
+
387
+ const template = await loadTemplate(configMode, paths);
388
+ template.plugin = [PACKAGE_NAME];
389
+
390
+ let nextConfig = template;
391
+ let existingConfig;
392
+ if (existsSync(paths.configPath)) {
393
+ const backupPath = await backupConfig(paths.configPath, dryRun);
394
+ log(`${dryRun ? "[dry-run] Would create backup" : "Backup created"}: ${backupPath}`);
395
+
396
+ try {
397
+ const existing = await readJson(paths.configPath);
398
+ existingConfig = existing;
399
+ const merged = { ...existing };
400
+ merged.plugin = normalizePluginList(existing.plugin);
401
+ const provider = (existing.provider && typeof existing.provider === "object")
402
+ ? { ...existing.provider }
403
+ : {};
404
+ provider.openai = mergeOpenaiProvider(existing.provider?.openai, template.provider?.openai);
405
+ merged.provider = provider;
406
+ nextConfig = merged;
407
+ } catch (error) {
408
+ log(`Warning: Could not parse existing config (${formatErrorForLog(error)}). Replacing with template.`);
409
+ existingConfig = undefined;
410
+ nextConfig = template;
411
+ }
412
+ } else {
413
+ log("No existing config found. Creating new global config.");
414
+ }
415
+
416
+ let wrote = false;
417
+ if (dryRun) {
418
+ log(`[dry-run] Would write ${paths.configPath} using ${configMode} config`);
419
+ log(`[dry-run] Diff for ${paths.configPath}:`);
420
+ log(formatConfigDiff(existingConfig, nextConfig));
421
+ } else {
422
+ await writeFileAtomic(paths.configPath, formatJson(nextConfig));
423
+ wrote = true;
424
+ log(`Wrote ${paths.configPath} (${configMode} config)`);
425
+ }
426
+
427
+ await clearCache(paths, dryRun, skipCacheClear);
428
+
429
+ log("\nDone. Restart OpenCode to (re)install the plugin.");
430
+ log("Example: opencode");
431
+ if (configMode === "modern") {
432
+ log("Note: Modern config intentionally shows 9 base model entries; use --variant to access all 34 shipped presets.");
433
+ }
434
+ if (configMode === "legacy") {
435
+ log("Note: Legacy config writes 34 explicit preset entries and is also safe for older OpenCode versions.");
436
+ }
437
+ if (configMode === "full") {
438
+ log("Note: Full config installs both modern base models and explicit preset entries so the full shipped catalog is visible by default.");
439
+ }
440
+
441
+ return {
442
+ exitCode: 0,
443
+ action: "install",
444
+ configMode,
445
+ configPath: paths.configPath,
446
+ dryRun: Boolean(dryRun),
447
+ wrote,
448
+ };
449
+ }
450
+
451
+ export const __test = {
452
+ buildPaths,
453
+ backupConfig,
454
+ copyFileWithWindowsRetry,
455
+ formatConfigDiff,
456
+ mergeFullTemplate,
457
+ mergeOpenaiProvider,
458
+ parseCliArgs,
459
+ writeFileAtomic,
460
+ renameWithWindowsRetry,
461
+ resolveHomeDirectory,
462
+ };