config.ts 46 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226
  1. import { Log } from "../util/log"
  2. import path from "path"
  3. import { pathToFileURL } from "url"
  4. import os from "os"
  5. import z from "zod"
  6. import { Filesystem } from "../util/filesystem"
  7. import { ModelsDev } from "../provider/models"
  8. import { mergeDeep, pipe, unique } from "remeda"
  9. import { Global } from "../global"
  10. import fs from "fs/promises"
  11. import { lazy } from "../util/lazy"
  12. import { NamedError } from "@opencode-ai/util/error"
  13. import { Flag } from "../flag/flag"
  14. import { Auth } from "../auth"
  15. import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser"
  16. import { Instance } from "../project/instance"
  17. import { LSPServer } from "../lsp/server"
  18. import { BunProc } from "@/bun"
  19. import { Installation } from "@/installation"
  20. import { ConfigMarkdown } from "./markdown"
  21. import { existsSync } from "fs"
  22. export namespace Config {
  23. const log = Log.create({ service: "config" })
  24. // Custom merge function that concatenates array fields instead of replacing them
  25. function mergeConfigConcatArrays(target: Info, source: Info): Info {
  26. const merged = mergeDeep(target, source)
  27. if (target.plugin && source.plugin) {
  28. merged.plugin = Array.from(new Set([...target.plugin, ...source.plugin]))
  29. }
  30. if (target.instructions && source.instructions) {
  31. merged.instructions = Array.from(new Set([...target.instructions, ...source.instructions]))
  32. }
  33. return merged
  34. }
  35. export const state = Instance.state(async () => {
  36. const auth = await Auth.all()
  37. // Load remote/well-known config first as the base layer (lowest precedence)
  38. // This allows organizations to provide default configs that users can override
  39. let result: Info = {}
  40. for (const [key, value] of Object.entries(auth)) {
  41. if (value.type === "wellknown") {
  42. process.env[value.key] = value.token
  43. log.debug("fetching remote config", { url: `${key}/.well-known/opencode` })
  44. const response = await fetch(`${key}/.well-known/opencode`)
  45. if (!response.ok) {
  46. throw new Error(`failed to fetch remote config from ${key}: ${response.status}`)
  47. }
  48. const wellknown = (await response.json()) as any
  49. const remoteConfig = wellknown.config ?? {}
  50. // Add $schema to prevent load() from trying to write back to a non-existent file
  51. if (!remoteConfig.$schema) remoteConfig.$schema = "https://opencode.ai/config.json"
  52. result = mergeConfigConcatArrays(
  53. result,
  54. await load(JSON.stringify(remoteConfig), `${key}/.well-known/opencode`),
  55. )
  56. log.debug("loaded remote config from well-known", { url: key })
  57. }
  58. }
  59. // Global user config overrides remote config
  60. result = mergeConfigConcatArrays(result, await global())
  61. // Custom config path overrides global
  62. if (Flag.OPENCODE_CONFIG) {
  63. result = mergeConfigConcatArrays(result, await loadFile(Flag.OPENCODE_CONFIG))
  64. log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
  65. }
  66. // Project config has highest precedence (overrides global and remote)
  67. for (const file of ["opencode.jsonc", "opencode.json"]) {
  68. const found = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
  69. for (const resolved of found.toReversed()) {
  70. result = mergeConfigConcatArrays(result, await loadFile(resolved))
  71. }
  72. }
  73. // Inline config content has highest precedence
  74. if (Flag.OPENCODE_CONFIG_CONTENT) {
  75. result = mergeConfigConcatArrays(result, JSON.parse(Flag.OPENCODE_CONFIG_CONTENT))
  76. log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
  77. }
  78. result.agent = result.agent || {}
  79. result.mode = result.mode || {}
  80. result.plugin = result.plugin || []
  81. const directories = [
  82. Global.Path.config,
  83. ...(await Array.fromAsync(
  84. Filesystem.up({
  85. targets: [".opencode"],
  86. start: Instance.directory,
  87. stop: Instance.worktree,
  88. }),
  89. )),
  90. ...(await Array.fromAsync(
  91. Filesystem.up({
  92. targets: [".opencode"],
  93. start: Global.Path.home,
  94. stop: Global.Path.home,
  95. }),
  96. )),
  97. ]
  98. if (Flag.OPENCODE_CONFIG_DIR) {
  99. directories.push(Flag.OPENCODE_CONFIG_DIR)
  100. log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR })
  101. }
  102. for (const dir of unique(directories)) {
  103. if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
  104. for (const file of ["opencode.jsonc", "opencode.json"]) {
  105. log.debug(`loading config from ${path.join(dir, file)}`)
  106. result = mergeConfigConcatArrays(result, await loadFile(path.join(dir, file)))
  107. // to satisfy the type checker
  108. result.agent ??= {}
  109. result.mode ??= {}
  110. result.plugin ??= []
  111. }
  112. }
  113. const exists = existsSync(path.join(dir, "node_modules"))
  114. const installing = installDependencies(dir)
  115. if (!exists) await installing
  116. result.command = mergeDeep(result.command ?? {}, await loadCommand(dir))
  117. result.agent = mergeDeep(result.agent, await loadAgent(dir))
  118. result.agent = mergeDeep(result.agent, await loadMode(dir))
  119. result.plugin.push(...(await loadPlugin(dir)))
  120. }
  121. // Migrate deprecated mode field to agent field
  122. for (const [name, mode] of Object.entries(result.mode)) {
  123. result.agent = mergeDeep(result.agent ?? {}, {
  124. [name]: {
  125. ...mode,
  126. mode: "primary" as const,
  127. },
  128. })
  129. }
  130. if (Flag.OPENCODE_PERMISSION) {
  131. result.permission = mergeDeep(result.permission ?? {}, JSON.parse(Flag.OPENCODE_PERMISSION))
  132. }
  133. // Backwards compatibility: legacy top-level `tools` config
  134. if (result.tools) {
  135. const perms: Record<string, Config.PermissionAction> = {}
  136. for (const [tool, enabled] of Object.entries(result.tools)) {
  137. const action: Config.PermissionAction = enabled ? "allow" : "deny"
  138. if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") {
  139. perms.edit = action
  140. continue
  141. }
  142. perms[tool] = action
  143. }
  144. result.permission = mergeDeep(perms, result.permission ?? {})
  145. }
  146. if (!result.username) result.username = os.userInfo().username
  147. // Handle migration from autoshare to share field
  148. if (result.autoshare === true && !result.share) {
  149. result.share = "auto"
  150. }
  151. if (!result.keybinds) result.keybinds = Info.shape.keybinds.parse({})
  152. // Apply flag overrides for compaction settings
  153. if (Flag.OPENCODE_DISABLE_AUTOCOMPACT) {
  154. result.compaction = { ...result.compaction, auto: false }
  155. }
  156. if (Flag.OPENCODE_DISABLE_PRUNE) {
  157. result.compaction = { ...result.compaction, prune: false }
  158. }
  159. result.plugin = deduplicatePlugins(result.plugin ?? [])
  160. return {
  161. config: result,
  162. directories,
  163. }
  164. })
  165. export async function installDependencies(dir: string) {
  166. const pkg = path.join(dir, "package.json")
  167. if (!(await Bun.file(pkg).exists())) {
  168. await Bun.write(pkg, "{}")
  169. }
  170. const gitignore = path.join(dir, ".gitignore")
  171. const hasGitIgnore = await Bun.file(gitignore).exists()
  172. if (!hasGitIgnore) await Bun.write(gitignore, ["node_modules", "package.json", "bun.lock", ".gitignore"].join("\n"))
  173. await BunProc.run(
  174. ["add", "@opencode-ai/plugin@" + (Installation.isLocal() ? "latest" : Installation.VERSION), "--exact"],
  175. {
  176. cwd: dir,
  177. },
  178. ).catch(() => {})
  179. // Install any additional dependencies defined in the package.json
  180. // This allows local plugins and custom tools to use external packages
  181. await BunProc.run(["install"], { cwd: dir }).catch(() => {})
  182. }
  183. const COMMAND_GLOB = new Bun.Glob("{command,commands}/**/*.md")
  184. async function loadCommand(dir: string) {
  185. const result: Record<string, Command> = {}
  186. for await (const item of COMMAND_GLOB.scan({
  187. absolute: true,
  188. followSymlinks: true,
  189. dot: true,
  190. cwd: dir,
  191. })) {
  192. const md = await ConfigMarkdown.parse(item)
  193. if (!md.data) continue
  194. const name = (() => {
  195. const patterns = ["/.opencode/command/", "/command/"]
  196. const pattern = patterns.find((p) => item.includes(p))
  197. if (pattern) {
  198. const index = item.indexOf(pattern)
  199. return item.slice(index + pattern.length, -3)
  200. }
  201. return path.basename(item, ".md")
  202. })()
  203. const config = {
  204. name,
  205. ...md.data,
  206. template: md.content.trim(),
  207. }
  208. const parsed = Command.safeParse(config)
  209. if (parsed.success) {
  210. result[config.name] = parsed.data
  211. continue
  212. }
  213. throw new InvalidError({ path: item, issues: parsed.error.issues }, { cause: parsed.error })
  214. }
  215. return result
  216. }
  217. const AGENT_GLOB = new Bun.Glob("{agent,agents}/**/*.md")
  218. async function loadAgent(dir: string) {
  219. const result: Record<string, Agent> = {}
  220. for await (const item of AGENT_GLOB.scan({
  221. absolute: true,
  222. followSymlinks: true,
  223. dot: true,
  224. cwd: dir,
  225. })) {
  226. const md = await ConfigMarkdown.parse(item)
  227. if (!md.data) continue
  228. // Extract relative path from agent folder for nested agents
  229. let agentName = path.basename(item, ".md")
  230. const agentFolderPath = item.includes("/.opencode/agent/")
  231. ? item.split("/.opencode/agent/")[1]
  232. : item.includes("/agent/")
  233. ? item.split("/agent/")[1]
  234. : agentName + ".md"
  235. // If agent is in a subfolder, include folder path in name
  236. if (agentFolderPath.includes("/")) {
  237. const relativePath = agentFolderPath.replace(".md", "")
  238. const pathParts = relativePath.split("/")
  239. agentName = pathParts.slice(0, -1).join("/") + "/" + pathParts[pathParts.length - 1]
  240. }
  241. const config = {
  242. name: agentName,
  243. ...md.data,
  244. prompt: md.content.trim(),
  245. }
  246. const parsed = Agent.safeParse(config)
  247. if (parsed.success) {
  248. result[config.name] = parsed.data
  249. continue
  250. }
  251. throw new InvalidError({ path: item, issues: parsed.error.issues }, { cause: parsed.error })
  252. }
  253. return result
  254. }
  255. const MODE_GLOB = new Bun.Glob("{mode,modes}/*.md")
  256. async function loadMode(dir: string) {
  257. const result: Record<string, Agent> = {}
  258. for await (const item of MODE_GLOB.scan({
  259. absolute: true,
  260. followSymlinks: true,
  261. dot: true,
  262. cwd: dir,
  263. })) {
  264. const md = await ConfigMarkdown.parse(item)
  265. if (!md.data) continue
  266. const config = {
  267. name: path.basename(item, ".md"),
  268. ...md.data,
  269. prompt: md.content.trim(),
  270. }
  271. const parsed = Agent.safeParse(config)
  272. if (parsed.success) {
  273. result[config.name] = {
  274. ...parsed.data,
  275. mode: "primary" as const,
  276. }
  277. continue
  278. }
  279. }
  280. return result
  281. }
  282. const PLUGIN_GLOB = new Bun.Glob("{plugin,plugins}/*.{ts,js}")
  283. async function loadPlugin(dir: string) {
  284. const plugins: string[] = []
  285. for await (const item of PLUGIN_GLOB.scan({
  286. absolute: true,
  287. followSymlinks: true,
  288. dot: true,
  289. cwd: dir,
  290. })) {
  291. plugins.push(pathToFileURL(item).href)
  292. }
  293. return plugins
  294. }
  295. /**
  296. * Extracts a canonical plugin name from a plugin specifier.
  297. * - For file:// URLs: extracts filename without extension
  298. * - For npm packages: extracts package name without version
  299. *
  300. * @example
  301. * getPluginName("file:///path/to/plugin/foo.js") // "foo"
  302. * getPluginName("[email protected]") // "oh-my-opencode"
  303. * getPluginName("@scope/[email protected]") // "@scope/pkg"
  304. */
  305. export function getPluginName(plugin: string): string {
  306. if (plugin.startsWith("file://")) {
  307. return path.parse(new URL(plugin).pathname).name
  308. }
  309. const lastAt = plugin.lastIndexOf("@")
  310. if (lastAt > 0) {
  311. return plugin.substring(0, lastAt)
  312. }
  313. return plugin
  314. }
  315. /**
  316. * Deduplicates plugins by name, with later entries (higher priority) winning.
  317. * Priority order (highest to lowest):
  318. * 1. Local plugin/ directory
  319. * 2. Local opencode.json
  320. * 3. Global plugin/ directory
  321. * 4. Global opencode.json
  322. *
  323. * Since plugins are added in low-to-high priority order,
  324. * we reverse, deduplicate (keeping first occurrence), then restore order.
  325. */
  326. export function deduplicatePlugins(plugins: string[]): string[] {
  327. // seenNames: canonical plugin names for duplicate detection
  328. // e.g., "oh-my-opencode", "@scope/pkg"
  329. const seenNames = new Set<string>()
  330. // uniqueSpecifiers: full plugin specifiers to return
  331. // e.g., "[email protected]", "file:///path/to/plugin.js"
  332. const uniqueSpecifiers: string[] = []
  333. for (const specifier of plugins.toReversed()) {
  334. const name = getPluginName(specifier)
  335. if (!seenNames.has(name)) {
  336. seenNames.add(name)
  337. uniqueSpecifiers.push(specifier)
  338. }
  339. }
  340. return uniqueSpecifiers.toReversed()
  341. }
  342. export const McpLocal = z
  343. .object({
  344. type: z.literal("local").describe("Type of MCP server connection"),
  345. command: z.string().array().describe("Command and arguments to run the MCP server"),
  346. environment: z
  347. .record(z.string(), z.string())
  348. .optional()
  349. .describe("Environment variables to set when running the MCP server"),
  350. enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
  351. timeout: z
  352. .number()
  353. .int()
  354. .positive()
  355. .optional()
  356. .describe(
  357. "Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified.",
  358. ),
  359. })
  360. .strict()
  361. .meta({
  362. ref: "McpLocalConfig",
  363. })
  364. export const McpOAuth = z
  365. .object({
  366. clientId: z
  367. .string()
  368. .optional()
  369. .describe("OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted."),
  370. clientSecret: z.string().optional().describe("OAuth client secret (if required by the authorization server)"),
  371. scope: z.string().optional().describe("OAuth scopes to request during authorization"),
  372. })
  373. .strict()
  374. .meta({
  375. ref: "McpOAuthConfig",
  376. })
  377. export type McpOAuth = z.infer<typeof McpOAuth>
  378. export const McpRemote = z
  379. .object({
  380. type: z.literal("remote").describe("Type of MCP server connection"),
  381. url: z.string().describe("URL of the remote MCP server"),
  382. enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
  383. headers: z.record(z.string(), z.string()).optional().describe("Headers to send with the request"),
  384. oauth: z
  385. .union([McpOAuth, z.literal(false)])
  386. .optional()
  387. .describe(
  388. "OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection.",
  389. ),
  390. timeout: z
  391. .number()
  392. .int()
  393. .positive()
  394. .optional()
  395. .describe(
  396. "Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified.",
  397. ),
  398. })
  399. .strict()
  400. .meta({
  401. ref: "McpRemoteConfig",
  402. })
  403. export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
  404. export type Mcp = z.infer<typeof Mcp>
  405. export const PermissionAction = z.enum(["ask", "allow", "deny"]).meta({
  406. ref: "PermissionActionConfig",
  407. })
  408. export type PermissionAction = z.infer<typeof PermissionAction>
  409. export const PermissionObject = z.record(z.string(), PermissionAction).meta({
  410. ref: "PermissionObjectConfig",
  411. })
  412. export type PermissionObject = z.infer<typeof PermissionObject>
  413. export const PermissionRule = z.union([PermissionAction, PermissionObject]).meta({
  414. ref: "PermissionRuleConfig",
  415. })
  416. export type PermissionRule = z.infer<typeof PermissionRule>
  417. // Capture original key order before zod reorders, then rebuild in original order
  418. const permissionPreprocess = (val: unknown) => {
  419. if (typeof val === "object" && val !== null && !Array.isArray(val)) {
  420. return { __originalKeys: Object.keys(val), ...val }
  421. }
  422. return val
  423. }
  424. const permissionTransform = (x: unknown): Record<string, PermissionRule> => {
  425. if (typeof x === "string") return { "*": x as PermissionAction }
  426. const obj = x as { __originalKeys?: string[] } & Record<string, unknown>
  427. const { __originalKeys, ...rest } = obj
  428. if (!__originalKeys) return rest as Record<string, PermissionRule>
  429. const result: Record<string, PermissionRule> = {}
  430. for (const key of __originalKeys) {
  431. if (key in rest) result[key] = rest[key] as PermissionRule
  432. }
  433. return result
  434. }
  435. export const Permission = z
  436. .preprocess(
  437. permissionPreprocess,
  438. z
  439. .object({
  440. __originalKeys: z.string().array().optional(),
  441. read: PermissionRule.optional(),
  442. edit: PermissionRule.optional(),
  443. glob: PermissionRule.optional(),
  444. grep: PermissionRule.optional(),
  445. list: PermissionRule.optional(),
  446. bash: PermissionRule.optional(),
  447. task: PermissionRule.optional(),
  448. external_directory: PermissionRule.optional(),
  449. todowrite: PermissionAction.optional(),
  450. todoread: PermissionAction.optional(),
  451. question: PermissionAction.optional(),
  452. webfetch: PermissionAction.optional(),
  453. websearch: PermissionAction.optional(),
  454. codesearch: PermissionAction.optional(),
  455. lsp: PermissionRule.optional(),
  456. doom_loop: PermissionAction.optional(),
  457. })
  458. .catchall(PermissionRule)
  459. .or(PermissionAction),
  460. )
  461. .transform(permissionTransform)
  462. .meta({
  463. ref: "PermissionConfig",
  464. })
  465. export type Permission = z.infer<typeof Permission>
  466. export const Command = z.object({
  467. template: z.string(),
  468. description: z.string().optional(),
  469. agent: z.string().optional(),
  470. model: z.string().optional(),
  471. subtask: z.boolean().optional(),
  472. })
  473. export type Command = z.infer<typeof Command>
  474. export const Agent = z
  475. .object({
  476. model: z.string().optional(),
  477. temperature: z.number().optional(),
  478. top_p: z.number().optional(),
  479. prompt: z.string().optional(),
  480. tools: z.record(z.string(), z.boolean()).optional().describe("@deprecated Use 'permission' field instead"),
  481. disable: z.boolean().optional(),
  482. description: z.string().optional().describe("Description of when to use the agent"),
  483. mode: z.enum(["subagent", "primary", "all"]).optional(),
  484. hidden: z
  485. .boolean()
  486. .optional()
  487. .describe("Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)"),
  488. options: z.record(z.string(), z.any()).optional(),
  489. color: z
  490. .string()
  491. .regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format")
  492. .optional()
  493. .describe("Hex color code for the agent (e.g., #FF5733)"),
  494. steps: z
  495. .number()
  496. .int()
  497. .positive()
  498. .optional()
  499. .describe("Maximum number of agentic iterations before forcing text-only response"),
  500. maxSteps: z.number().int().positive().optional().describe("@deprecated Use 'steps' field instead."),
  501. permission: Permission.optional(),
  502. })
  503. .catchall(z.any())
  504. .transform((agent, ctx) => {
  505. const knownKeys = new Set([
  506. "name",
  507. "model",
  508. "prompt",
  509. "description",
  510. "temperature",
  511. "top_p",
  512. "mode",
  513. "hidden",
  514. "color",
  515. "steps",
  516. "maxSteps",
  517. "options",
  518. "permission",
  519. "disable",
  520. "tools",
  521. ])
  522. // Extract unknown properties into options
  523. const options: Record<string, unknown> = { ...agent.options }
  524. for (const [key, value] of Object.entries(agent)) {
  525. if (!knownKeys.has(key)) options[key] = value
  526. }
  527. // Convert legacy tools config to permissions
  528. const permission: Permission = {}
  529. for (const [tool, enabled] of Object.entries(agent.tools ?? {})) {
  530. const action = enabled ? "allow" : "deny"
  531. // write, edit, patch, multiedit all map to edit permission
  532. if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") {
  533. permission.edit = action
  534. } else {
  535. permission[tool] = action
  536. }
  537. }
  538. Object.assign(permission, agent.permission)
  539. // Convert legacy maxSteps to steps
  540. const steps = agent.steps ?? agent.maxSteps
  541. return { ...agent, options, permission, steps } as typeof agent & {
  542. options?: Record<string, unknown>
  543. permission?: Permission
  544. steps?: number
  545. }
  546. })
  547. .meta({
  548. ref: "AgentConfig",
  549. })
  550. export type Agent = z.infer<typeof Agent>
  551. export const Keybinds = z
  552. .object({
  553. leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"),
  554. app_exit: z.string().optional().default("ctrl+c,ctrl+d,<leader>q").describe("Exit the application"),
  555. editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
  556. theme_list: z.string().optional().default("<leader>t").describe("List available themes"),
  557. sidebar_toggle: z.string().optional().default("<leader>b").describe("Toggle sidebar"),
  558. scrollbar_toggle: z.string().optional().default("none").describe("Toggle session scrollbar"),
  559. username_toggle: z.string().optional().default("none").describe("Toggle username visibility"),
  560. status_view: z.string().optional().default("<leader>s").describe("View status"),
  561. session_export: z.string().optional().default("<leader>x").describe("Export session to editor"),
  562. session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
  563. session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
  564. session_timeline: z.string().optional().default("<leader>g").describe("Show session timeline"),
  565. session_fork: z.string().optional().default("none").describe("Fork session from message"),
  566. session_rename: z.string().optional().default("none").describe("Rename session"),
  567. session_share: z.string().optional().default("none").describe("Share current session"),
  568. session_unshare: z.string().optional().default("none").describe("Unshare current session"),
  569. session_interrupt: z.string().optional().default("escape").describe("Interrupt current session"),
  570. session_compact: z.string().optional().default("<leader>c").describe("Compact the session"),
  571. messages_page_up: z.string().optional().default("pageup").describe("Scroll messages up by one page"),
  572. messages_page_down: z.string().optional().default("pagedown").describe("Scroll messages down by one page"),
  573. messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"),
  574. messages_half_page_down: z
  575. .string()
  576. .optional()
  577. .default("ctrl+alt+d")
  578. .describe("Scroll messages down by half page"),
  579. messages_first: z.string().optional().default("ctrl+g,home").describe("Navigate to first message"),
  580. messages_last: z.string().optional().default("ctrl+alt+g,end").describe("Navigate to last message"),
  581. messages_next: z.string().optional().default("none").describe("Navigate to next message"),
  582. messages_previous: z.string().optional().default("none").describe("Navigate to previous message"),
  583. messages_last_user: z.string().optional().default("none").describe("Navigate to last user message"),
  584. messages_copy: z.string().optional().default("<leader>y").describe("Copy message"),
  585. messages_undo: z.string().optional().default("<leader>u").describe("Undo message"),
  586. messages_redo: z.string().optional().default("<leader>r").describe("Redo message"),
  587. messages_toggle_conceal: z
  588. .string()
  589. .optional()
  590. .default("<leader>h")
  591. .describe("Toggle code block concealment in messages"),
  592. tool_details: z.string().optional().default("none").describe("Toggle tool details visibility"),
  593. model_list: z.string().optional().default("<leader>m").describe("List available models"),
  594. model_cycle_recent: z.string().optional().default("f2").describe("Next recently used model"),
  595. model_cycle_recent_reverse: z.string().optional().default("shift+f2").describe("Previous recently used model"),
  596. model_cycle_favorite: z.string().optional().default("none").describe("Next favorite model"),
  597. model_cycle_favorite_reverse: z.string().optional().default("none").describe("Previous favorite model"),
  598. command_list: z.string().optional().default("ctrl+p").describe("List available commands"),
  599. agent_list: z.string().optional().default("<leader>a").describe("List agents"),
  600. agent_cycle: z.string().optional().default("tab").describe("Next agent"),
  601. agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
  602. variant_cycle: z.string().optional().default("ctrl+t").describe("Cycle model variants"),
  603. input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
  604. input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
  605. input_submit: z.string().optional().default("return").describe("Submit input"),
  606. input_newline: z
  607. .string()
  608. .optional()
  609. .default("shift+return,ctrl+return,alt+return,ctrl+j")
  610. .describe("Insert newline in input"),
  611. input_move_left: z.string().optional().default("left,ctrl+b").describe("Move cursor left in input"),
  612. input_move_right: z.string().optional().default("right,ctrl+f").describe("Move cursor right in input"),
  613. input_move_up: z.string().optional().default("up").describe("Move cursor up in input"),
  614. input_move_down: z.string().optional().default("down").describe("Move cursor down in input"),
  615. input_select_left: z.string().optional().default("shift+left").describe("Select left in input"),
  616. input_select_right: z.string().optional().default("shift+right").describe("Select right in input"),
  617. input_select_up: z.string().optional().default("shift+up").describe("Select up in input"),
  618. input_select_down: z.string().optional().default("shift+down").describe("Select down in input"),
  619. input_line_home: z.string().optional().default("ctrl+a").describe("Move to start of line in input"),
  620. input_line_end: z.string().optional().default("ctrl+e").describe("Move to end of line in input"),
  621. input_select_line_home: z
  622. .string()
  623. .optional()
  624. .default("ctrl+shift+a")
  625. .describe("Select to start of line in input"),
  626. input_select_line_end: z.string().optional().default("ctrl+shift+e").describe("Select to end of line in input"),
  627. input_visual_line_home: z.string().optional().default("alt+a").describe("Move to start of visual line in input"),
  628. input_visual_line_end: z.string().optional().default("alt+e").describe("Move to end of visual line in input"),
  629. input_select_visual_line_home: z
  630. .string()
  631. .optional()
  632. .default("alt+shift+a")
  633. .describe("Select to start of visual line in input"),
  634. input_select_visual_line_end: z
  635. .string()
  636. .optional()
  637. .default("alt+shift+e")
  638. .describe("Select to end of visual line in input"),
  639. input_buffer_home: z.string().optional().default("home").describe("Move to start of buffer in input"),
  640. input_buffer_end: z.string().optional().default("end").describe("Move to end of buffer in input"),
  641. input_select_buffer_home: z
  642. .string()
  643. .optional()
  644. .default("shift+home")
  645. .describe("Select to start of buffer in input"),
  646. input_select_buffer_end: z.string().optional().default("shift+end").describe("Select to end of buffer in input"),
  647. input_delete_line: z.string().optional().default("ctrl+shift+d").describe("Delete line in input"),
  648. input_delete_to_line_end: z.string().optional().default("ctrl+k").describe("Delete to end of line in input"),
  649. input_delete_to_line_start: z.string().optional().default("ctrl+u").describe("Delete to start of line in input"),
  650. input_backspace: z.string().optional().default("backspace,shift+backspace").describe("Backspace in input"),
  651. input_delete: z.string().optional().default("ctrl+d,delete,shift+delete").describe("Delete character in input"),
  652. input_undo: z.string().optional().default("ctrl+-,super+z").describe("Undo in input"),
  653. input_redo: z.string().optional().default("ctrl+.,super+shift+z").describe("Redo in input"),
  654. input_word_forward: z
  655. .string()
  656. .optional()
  657. .default("alt+f,alt+right,ctrl+right")
  658. .describe("Move word forward in input"),
  659. input_word_backward: z
  660. .string()
  661. .optional()
  662. .default("alt+b,alt+left,ctrl+left")
  663. .describe("Move word backward in input"),
  664. input_select_word_forward: z
  665. .string()
  666. .optional()
  667. .default("alt+shift+f,alt+shift+right")
  668. .describe("Select word forward in input"),
  669. input_select_word_backward: z
  670. .string()
  671. .optional()
  672. .default("alt+shift+b,alt+shift+left")
  673. .describe("Select word backward in input"),
  674. input_delete_word_forward: z
  675. .string()
  676. .optional()
  677. .default("alt+d,alt+delete,ctrl+delete")
  678. .describe("Delete word forward in input"),
  679. input_delete_word_backward: z
  680. .string()
  681. .optional()
  682. .default("ctrl+w,ctrl+backspace,alt+backspace")
  683. .describe("Delete word backward in input"),
  684. history_previous: z.string().optional().default("up").describe("Previous history item"),
  685. history_next: z.string().optional().default("down").describe("Next history item"),
  686. session_child_cycle: z.string().optional().default("<leader>right").describe("Next child session"),
  687. session_child_cycle_reverse: z.string().optional().default("<leader>left").describe("Previous child session"),
  688. session_parent: z.string().optional().default("<leader>up").describe("Go to parent session"),
  689. terminal_suspend: z.string().optional().default("ctrl+z").describe("Suspend terminal"),
  690. terminal_title_toggle: z.string().optional().default("none").describe("Toggle terminal title"),
  691. tips_toggle: z.string().optional().default("<leader>h").describe("Toggle tips on home screen"),
  692. })
  693. .strict()
  694. .meta({
  695. ref: "KeybindsConfig",
  696. })
  697. export const TUI = z.object({
  698. scroll_speed: z.number().min(0.001).optional().describe("TUI scroll speed"),
  699. scroll_acceleration: z
  700. .object({
  701. enabled: z.boolean().describe("Enable scroll acceleration"),
  702. })
  703. .optional()
  704. .describe("Scroll acceleration settings"),
  705. diff_style: z
  706. .enum(["auto", "stacked"])
  707. .optional()
  708. .describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"),
  709. })
  710. export const Server = z
  711. .object({
  712. port: z.number().int().positive().optional().describe("Port to listen on"),
  713. hostname: z.string().optional().describe("Hostname to listen on"),
  714. mdns: z.boolean().optional().describe("Enable mDNS service discovery"),
  715. cors: z.array(z.string()).optional().describe("Additional domains to allow for CORS"),
  716. })
  717. .strict()
  718. .meta({
  719. ref: "ServerConfig",
  720. })
  721. export const Layout = z.enum(["auto", "stretch"]).meta({
  722. ref: "LayoutConfig",
  723. })
  724. export type Layout = z.infer<typeof Layout>
  725. export const Provider = ModelsDev.Provider.partial()
  726. .extend({
  727. whitelist: z.array(z.string()).optional(),
  728. blacklist: z.array(z.string()).optional(),
  729. models: z
  730. .record(
  731. z.string(),
  732. ModelsDev.Model.partial().extend({
  733. variants: z
  734. .record(
  735. z.string(),
  736. z
  737. .object({
  738. disabled: z.boolean().optional().describe("Disable this variant for the model"),
  739. })
  740. .catchall(z.any()),
  741. )
  742. .optional()
  743. .describe("Variant-specific configuration"),
  744. }),
  745. )
  746. .optional(),
  747. options: z
  748. .object({
  749. apiKey: z.string().optional(),
  750. baseURL: z.string().optional(),
  751. enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"),
  752. setCacheKey: z.boolean().optional().describe("Enable promptCacheKey for this provider (default false)"),
  753. timeout: z
  754. .union([
  755. z
  756. .number()
  757. .int()
  758. .positive()
  759. .describe(
  760. "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
  761. ),
  762. z.literal(false).describe("Disable timeout for this provider entirely."),
  763. ])
  764. .optional()
  765. .describe(
  766. "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
  767. ),
  768. })
  769. .catchall(z.any())
  770. .optional(),
  771. })
  772. .strict()
  773. .meta({
  774. ref: "ProviderConfig",
  775. })
  776. export type Provider = z.infer<typeof Provider>
  777. export const Info = z
  778. .object({
  779. $schema: z.string().optional().describe("JSON schema reference for configuration validation"),
  780. theme: z.string().optional().describe("Theme name to use for the interface"),
  781. keybinds: Keybinds.optional().describe("Custom keybind configurations"),
  782. logLevel: Log.Level.optional().describe("Log level"),
  783. tui: TUI.optional().describe("TUI specific settings"),
  784. server: Server.optional().describe("Server configuration for opencode serve and web commands"),
  785. command: z
  786. .record(z.string(), Command)
  787. .optional()
  788. .describe("Command configuration, see https://opencode.ai/docs/commands"),
  789. watcher: z
  790. .object({
  791. ignore: z.array(z.string()).optional(),
  792. })
  793. .optional(),
  794. plugin: z.string().array().optional(),
  795. snapshot: z.boolean().optional(),
  796. share: z
  797. .enum(["manual", "auto", "disabled"])
  798. .optional()
  799. .describe(
  800. "Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing",
  801. ),
  802. autoshare: z
  803. .boolean()
  804. .optional()
  805. .describe("@deprecated Use 'share' field instead. Share newly created sessions automatically"),
  806. autoupdate: z
  807. .union([z.boolean(), z.literal("notify")])
  808. .optional()
  809. .describe(
  810. "Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications",
  811. ),
  812. disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"),
  813. enabled_providers: z
  814. .array(z.string())
  815. .optional()
  816. .describe("When set, ONLY these providers will be enabled. All other providers will be ignored"),
  817. model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
  818. small_model: z
  819. .string()
  820. .describe("Small model to use for tasks like title generation in the format of provider/model")
  821. .optional(),
  822. default_agent: z
  823. .string()
  824. .optional()
  825. .describe(
  826. "Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.",
  827. ),
  828. username: z
  829. .string()
  830. .optional()
  831. .describe("Custom username to display in conversations instead of system username"),
  832. mode: z
  833. .object({
  834. build: Agent.optional(),
  835. plan: Agent.optional(),
  836. })
  837. .catchall(Agent)
  838. .optional()
  839. .describe("@deprecated Use `agent` field instead."),
  840. agent: z
  841. .object({
  842. // primary
  843. plan: Agent.optional(),
  844. build: Agent.optional(),
  845. // subagent
  846. general: Agent.optional(),
  847. explore: Agent.optional(),
  848. // specialized
  849. title: Agent.optional(),
  850. summary: Agent.optional(),
  851. compaction: Agent.optional(),
  852. })
  853. .catchall(Agent)
  854. .optional()
  855. .describe("Agent configuration, see https://opencode.ai/docs/agent"),
  856. provider: z
  857. .record(z.string(), Provider)
  858. .optional()
  859. .describe("Custom provider configurations and model overrides"),
  860. mcp: z
  861. .record(
  862. z.string(),
  863. z.union([
  864. Mcp,
  865. z
  866. .object({
  867. enabled: z.boolean(),
  868. })
  869. .strict(),
  870. ]),
  871. )
  872. .optional()
  873. .describe("MCP (Model Context Protocol) server configurations"),
  874. formatter: z
  875. .union([
  876. z.literal(false),
  877. z.record(
  878. z.string(),
  879. z.object({
  880. disabled: z.boolean().optional(),
  881. command: z.array(z.string()).optional(),
  882. environment: z.record(z.string(), z.string()).optional(),
  883. extensions: z.array(z.string()).optional(),
  884. }),
  885. ),
  886. ])
  887. .optional(),
  888. lsp: z
  889. .union([
  890. z.literal(false),
  891. z.record(
  892. z.string(),
  893. z.union([
  894. z.object({
  895. disabled: z.literal(true),
  896. }),
  897. z.object({
  898. command: z.array(z.string()),
  899. extensions: z.array(z.string()).optional(),
  900. disabled: z.boolean().optional(),
  901. env: z.record(z.string(), z.string()).optional(),
  902. initialization: z.record(z.string(), z.any()).optional(),
  903. }),
  904. ]),
  905. ),
  906. ])
  907. .optional()
  908. .refine(
  909. (data) => {
  910. if (!data) return true
  911. if (typeof data === "boolean") return true
  912. const serverIds = new Set(Object.values(LSPServer).map((s) => s.id))
  913. return Object.entries(data).every(([id, config]) => {
  914. if (config.disabled) return true
  915. if (serverIds.has(id)) return true
  916. return Boolean(config.extensions)
  917. })
  918. },
  919. {
  920. error: "For custom LSP servers, 'extensions' array is required.",
  921. },
  922. ),
  923. instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
  924. layout: Layout.optional().describe("@deprecated Always uses stretch layout."),
  925. permission: Permission.optional(),
  926. tools: z.record(z.string(), z.boolean()).optional(),
  927. enterprise: z
  928. .object({
  929. url: z.string().optional().describe("Enterprise URL"),
  930. })
  931. .optional(),
  932. compaction: z
  933. .object({
  934. auto: z.boolean().optional().describe("Enable automatic compaction when context is full (default: true)"),
  935. prune: z.boolean().optional().describe("Enable pruning of old tool outputs (default: true)"),
  936. })
  937. .optional(),
  938. experimental: z
  939. .object({
  940. hook: z
  941. .object({
  942. file_edited: z
  943. .record(
  944. z.string(),
  945. z
  946. .object({
  947. command: z.string().array(),
  948. environment: z.record(z.string(), z.string()).optional(),
  949. })
  950. .array(),
  951. )
  952. .optional(),
  953. session_completed: z
  954. .object({
  955. command: z.string().array(),
  956. environment: z.record(z.string(), z.string()).optional(),
  957. })
  958. .array()
  959. .optional(),
  960. })
  961. .optional(),
  962. chatMaxRetries: z.number().optional().describe("Number of retries for chat completions on failure"),
  963. disable_paste_summary: z.boolean().optional(),
  964. batch_tool: z.boolean().optional().describe("Enable the batch tool"),
  965. openTelemetry: z
  966. .boolean()
  967. .optional()
  968. .describe("Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)"),
  969. primary_tools: z
  970. .array(z.string())
  971. .optional()
  972. .describe("Tools that should only be available to primary agents."),
  973. continue_loop_on_deny: z.boolean().optional().describe("Continue the agent loop when a tool call is denied"),
  974. mcp_timeout: z
  975. .number()
  976. .int()
  977. .positive()
  978. .optional()
  979. .describe("Timeout in milliseconds for model context protocol (MCP) requests"),
  980. })
  981. .optional(),
  982. })
  983. .strict()
  984. .meta({
  985. ref: "Config",
  986. })
  987. export type Info = z.output<typeof Info>
  988. export const global = lazy(async () => {
  989. let result: Info = pipe(
  990. {},
  991. mergeDeep(await loadFile(path.join(Global.Path.config, "config.json"))),
  992. mergeDeep(await loadFile(path.join(Global.Path.config, "opencode.json"))),
  993. mergeDeep(await loadFile(path.join(Global.Path.config, "opencode.jsonc"))),
  994. )
  995. await import(path.join(Global.Path.config, "config"), {
  996. with: {
  997. type: "toml",
  998. },
  999. })
  1000. .then(async (mod) => {
  1001. const { provider, model, ...rest } = mod.default
  1002. if (provider && model) result.model = `${provider}/${model}`
  1003. result["$schema"] = "https://opencode.ai/config.json"
  1004. result = mergeDeep(result, rest)
  1005. await Bun.write(path.join(Global.Path.config, "config.json"), JSON.stringify(result, null, 2))
  1006. await fs.unlink(path.join(Global.Path.config, "config"))
  1007. })
  1008. .catch(() => {})
  1009. return result
  1010. })
  1011. async function loadFile(filepath: string): Promise<Info> {
  1012. log.info("loading", { path: filepath })
  1013. let text = await Bun.file(filepath)
  1014. .text()
  1015. .catch((err) => {
  1016. if (err.code === "ENOENT") return
  1017. throw new JsonError({ path: filepath }, { cause: err })
  1018. })
  1019. if (!text) return {}
  1020. return load(text, filepath)
  1021. }
  1022. async function load(text: string, configFilepath: string) {
  1023. text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
  1024. return process.env[varName] || ""
  1025. })
  1026. const fileMatches = text.match(/\{file:[^}]+\}/g)
  1027. if (fileMatches) {
  1028. const configDir = path.dirname(configFilepath)
  1029. const lines = text.split("\n")
  1030. for (const match of fileMatches) {
  1031. const lineIndex = lines.findIndex((line) => line.includes(match))
  1032. if (lineIndex !== -1 && lines[lineIndex].trim().startsWith("//")) {
  1033. continue // Skip if line is commented
  1034. }
  1035. let filePath = match.replace(/^\{file:/, "").replace(/\}$/, "")
  1036. if (filePath.startsWith("~/")) {
  1037. filePath = path.join(os.homedir(), filePath.slice(2))
  1038. }
  1039. const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
  1040. const fileContent = (
  1041. await Bun.file(resolvedPath)
  1042. .text()
  1043. .catch((error) => {
  1044. const errMsg = `bad file reference: "${match}"`
  1045. if (error.code === "ENOENT") {
  1046. throw new InvalidError(
  1047. {
  1048. path: configFilepath,
  1049. message: errMsg + ` ${resolvedPath} does not exist`,
  1050. },
  1051. { cause: error },
  1052. )
  1053. }
  1054. throw new InvalidError({ path: configFilepath, message: errMsg }, { cause: error })
  1055. })
  1056. ).trim()
  1057. // escape newlines/quotes, strip outer quotes
  1058. text = text.replace(match, JSON.stringify(fileContent).slice(1, -1))
  1059. }
  1060. }
  1061. const errors: JsoncParseError[] = []
  1062. const data = parseJsonc(text, errors, { allowTrailingComma: true })
  1063. if (errors.length) {
  1064. const lines = text.split("\n")
  1065. const errorDetails = errors
  1066. .map((e) => {
  1067. const beforeOffset = text.substring(0, e.offset).split("\n")
  1068. const line = beforeOffset.length
  1069. const column = beforeOffset[beforeOffset.length - 1].length + 1
  1070. const problemLine = lines[line - 1]
  1071. const error = `${printParseErrorCode(e.error)} at line ${line}, column ${column}`
  1072. if (!problemLine) return error
  1073. return `${error}\n Line ${line}: ${problemLine}\n${"".padStart(column + 9)}^`
  1074. })
  1075. .join("\n")
  1076. throw new JsonError({
  1077. path: configFilepath,
  1078. message: `\n--- JSONC Input ---\n${text}\n--- Errors ---\n${errorDetails}\n--- End ---`,
  1079. })
  1080. }
  1081. const parsed = Info.safeParse(data)
  1082. if (parsed.success) {
  1083. if (!parsed.data.$schema) {
  1084. parsed.data.$schema = "https://opencode.ai/config.json"
  1085. await Bun.write(configFilepath, JSON.stringify(parsed.data, null, 2))
  1086. }
  1087. const data = parsed.data
  1088. if (data.plugin) {
  1089. for (let i = 0; i < data.plugin.length; i++) {
  1090. const plugin = data.plugin[i]
  1091. try {
  1092. data.plugin[i] = import.meta.resolve!(plugin, configFilepath)
  1093. } catch (err) {}
  1094. }
  1095. }
  1096. return data
  1097. }
  1098. throw new InvalidError({
  1099. path: configFilepath,
  1100. issues: parsed.error.issues,
  1101. })
  1102. }
  1103. export const JsonError = NamedError.create(
  1104. "ConfigJsonError",
  1105. z.object({
  1106. path: z.string(),
  1107. message: z.string().optional(),
  1108. }),
  1109. )
  1110. export const ConfigDirectoryTypoError = NamedError.create(
  1111. "ConfigDirectoryTypoError",
  1112. z.object({
  1113. path: z.string(),
  1114. dir: z.string(),
  1115. suggestion: z.string(),
  1116. }),
  1117. )
  1118. export const InvalidError = NamedError.create(
  1119. "ConfigInvalidError",
  1120. z.object({
  1121. path: z.string(),
  1122. issues: z.custom<z.core.$ZodIssue[]>().optional(),
  1123. message: z.string().optional(),
  1124. }),
  1125. )
  1126. export async function get() {
  1127. return state().then((x) => x.config)
  1128. }
  1129. export async function update(config: Info) {
  1130. const filepath = path.join(Instance.directory, "config.json")
  1131. const existing = await loadFile(filepath)
  1132. await Bun.write(filepath, JSON.stringify(mergeDeep(existing, config), null, 2))
  1133. await Instance.dispose()
  1134. }
  1135. export async function directories() {
  1136. return state().then((x) => x.directories)
  1137. }
  1138. }