@@ -8,34 +8,17 @@ import { mergeDeep } from "remeda"
88import { Global } from "../global"
99import fs from "fs/promises"
1010import { lazy } from "../util/lazy"
11+ import { NamedError } from "../util/error"
1112
1213export namespace Config {
1314 const log = Log . create ( { service : "config" } )
1415
1516 export const state = App . state ( "config" , async ( app ) => {
1617 let result = await global ( )
1718 for ( const file of [ "opencode.jsonc" , "opencode.json" ] ) {
18- const [ resolved ] = await Filesystem . findUp (
19- file ,
20- app . path . cwd ,
21- app . path . root ,
22- )
23- if ( ! resolved ) continue
24- try {
25- result = mergeDeep (
26- result ,
27- await import ( resolved ) . then ( ( mod ) => Info . parse ( mod . default ) ) ,
28- )
29- log . info ( "found" , { path : resolved } )
30- break
31- } catch ( e ) {
32- if ( e instanceof z . ZodError ) {
33- for ( const issue of e . issues ) {
34- log . info ( issue . message )
35- }
36- throw e
37- }
38- continue
19+ const found = await Filesystem . findUp ( file , app . path . cwd , app . path . root )
20+ for ( const resolved of found . toReversed ( ) ) {
21+ result = mergeDeep ( result , await load ( resolved ) )
3922 }
4023 }
4124 log . info ( "loaded" , result )
@@ -45,9 +28,16 @@ export namespace Config {
4528 export const McpLocal = z
4629 . object ( {
4730 type : z . literal ( "local" ) . describe ( "Type of MCP server connection" ) ,
48- command : z . string ( ) . array ( ) . describe ( "Command and arguments to run the MCP server" ) ,
49- environment : z . record ( z . string ( ) , z . string ( ) ) . optional ( ) . describe ( "Environment variables to set when running the MCP server" ) ,
31+ command : z
32+ . string ( )
33+ . array ( )
34+ . describe ( "Command and arguments to run the MCP server" ) ,
35+ environment : z
36+ . record ( z . string ( ) , z . string ( ) )
37+ . optional ( )
38+ . describe ( "Environment variables to set when running the MCP server" ) ,
5039 } )
40+ . strict ( )
5141 . openapi ( {
5242 ref : "Config.McpLocal" ,
5343 } )
@@ -57,6 +47,7 @@ export namespace Config {
5747 type : z . literal ( "remote" ) . describe ( "Type of MCP server connection" ) ,
5848 url : z . string ( ) . describe ( "URL of the remote MCP server" ) ,
5949 } )
50+ . strict ( )
6051 . openapi ( {
6152 ref : "Config.McpRemote" ,
6253 } )
@@ -66,41 +57,84 @@ export namespace Config {
6657
6758 export const Keybinds = z
6859 . object ( {
69- leader : z . string ( ) . optional ( ) . describe ( "Leader key for keybind combinations" ) ,
60+ leader : z
61+ . string ( )
62+ . optional ( )
63+ . describe ( "Leader key for keybind combinations" ) ,
7064 help : z . string ( ) . optional ( ) . describe ( "Show help dialog" ) ,
7165 editor_open : z . string ( ) . optional ( ) . describe ( "Open external editor" ) ,
7266 session_new : z . string ( ) . optional ( ) . describe ( "Create a new session" ) ,
7367 session_list : z . string ( ) . optional ( ) . describe ( "List all sessions" ) ,
7468 session_share : z . string ( ) . optional ( ) . describe ( "Share current session" ) ,
75- session_interrupt : z . string ( ) . optional ( ) . describe ( "Interrupt current session" ) ,
76- session_compact : z . string ( ) . optional ( ) . describe ( "Toggle compact mode for session" ) ,
69+ session_interrupt : z
70+ . string ( )
71+ . optional ( )
72+ . describe ( "Interrupt current session" ) ,
73+ session_compact : z
74+ . string ( )
75+ . optional ( )
76+ . describe ( "Toggle compact mode for session" ) ,
7777 tool_details : z . string ( ) . optional ( ) . describe ( "Show tool details" ) ,
7878 model_list : z . string ( ) . optional ( ) . describe ( "List available models" ) ,
7979 theme_list : z . string ( ) . optional ( ) . describe ( "List available themes" ) ,
80- project_init : z . string ( ) . optional ( ) . describe ( "Initialize project configuration" ) ,
80+ project_init : z
81+ . string ( )
82+ . optional ( )
83+ . describe ( "Initialize project configuration" ) ,
8184 input_clear : z . string ( ) . optional ( ) . describe ( "Clear input field" ) ,
8285 input_paste : z . string ( ) . optional ( ) . describe ( "Paste from clipboard" ) ,
8386 input_submit : z . string ( ) . optional ( ) . describe ( "Submit input" ) ,
8487 input_newline : z . string ( ) . optional ( ) . describe ( "Insert newline in input" ) ,
85- history_previous : z . string ( ) . optional ( ) . describe ( "Navigate to previous history item" ) ,
86- history_next : z . string ( ) . optional ( ) . describe ( "Navigate to next history item" ) ,
87- messages_page_up : z . string ( ) . optional ( ) . describe ( "Scroll messages up by one page" ) ,
88- messages_page_down : z . string ( ) . optional ( ) . describe ( "Scroll messages down by one page" ) ,
89- messages_half_page_up : z . string ( ) . optional ( ) . describe ( "Scroll messages up by half page" ) ,
90- messages_half_page_down : z . string ( ) . optional ( ) . describe ( "Scroll messages down by half page" ) ,
91- messages_previous : z . string ( ) . optional ( ) . describe ( "Navigate to previous message" ) ,
88+ history_previous : z
89+ . string ( )
90+ . optional ( )
91+ . describe ( "Navigate to previous history item" ) ,
92+ history_next : z
93+ . string ( )
94+ . optional ( )
95+ . describe ( "Navigate to next history item" ) ,
96+ messages_page_up : z
97+ . string ( )
98+ . optional ( )
99+ . describe ( "Scroll messages up by one page" ) ,
100+ messages_page_down : z
101+ . string ( )
102+ . optional ( )
103+ . describe ( "Scroll messages down by one page" ) ,
104+ messages_half_page_up : z
105+ . string ( )
106+ . optional ( )
107+ . describe ( "Scroll messages up by half page" ) ,
108+ messages_half_page_down : z
109+ . string ( )
110+ . optional ( )
111+ . describe ( "Scroll messages down by half page" ) ,
112+ messages_previous : z
113+ . string ( )
114+ . optional ( )
115+ . describe ( "Navigate to previous message" ) ,
92116 messages_next : z . string ( ) . optional ( ) . describe ( "Navigate to next message" ) ,
93- messages_first : z . string ( ) . optional ( ) . describe ( "Navigate to first message" ) ,
117+ messages_first : z
118+ . string ( )
119+ . optional ( )
120+ . describe ( "Navigate to first message" ) ,
94121 messages_last : z . string ( ) . optional ( ) . describe ( "Navigate to last message" ) ,
95122 app_exit : z . string ( ) . optional ( ) . describe ( "Exit the application" ) ,
96123 } )
124+ . strict ( )
97125 . openapi ( {
98126 ref : "Config.Keybinds" ,
99127 } )
100128 export const Info = z
101129 . object ( {
102- $schema : z . string ( ) . optional ( ) . describe ( "JSON schema reference for configuration validation" ) ,
103- theme : z . string ( ) . optional ( ) . describe ( "Theme name to use for the interface" ) ,
130+ $schema : z
131+ . string ( )
132+ . optional ( )
133+ . describe ( "JSON schema reference for configuration validation" ) ,
134+ theme : z
135+ . string ( )
136+ . optional ( )
137+ . describe ( "Theme name to use for the interface" ) ,
104138 keybinds : Keybinds . optional ( ) . describe ( "Custom keybind configurations" ) ,
105139 autoshare : z
106140 . boolean ( )
@@ -129,19 +163,20 @@ export namespace Config {
129163 )
130164 . optional ( )
131165 . describe ( "Custom provider configurations and model overrides" ) ,
132- mcp : z . record ( z . string ( ) , Mcp ) . optional ( ) . describe ( "MCP (Model Context Protocol) server configurations" ) ,
166+ mcp : z
167+ . record ( z . string ( ) , Mcp )
168+ . optional ( )
169+ . describe ( "MCP (Model Context Protocol) server configurations" ) ,
133170 } )
171+ . strict ( )
134172 . openapi ( {
135173 ref : "Config.Info" ,
136174 } )
137175
138176 export type Info = z . output < typeof Info >
139177
140178 export const global = lazy ( async ( ) => {
141- let result = await Bun . file ( path . join ( Global . Path . config , "config.json" ) )
142- . json ( )
143- . then ( ( mod ) => Info . parse ( mod ) )
144- . catch ( ( ) => ( { } ) as Info )
179+ let result = await load ( path . join ( Global . Path . config , "config.json" ) )
145180
146181 await import ( path . join ( Global . Path . config , "config" ) , {
147182 with : {
@@ -160,9 +195,38 @@ export namespace Config {
160195 await fs . unlink ( path . join ( Global . Path . config , "config" ) )
161196 } )
162197 . catch ( ( ) => { } )
163- return Info . parse ( result )
198+
199+ return result
164200 } )
165201
202+ async function load ( path : string ) {
203+ const data = await Bun . file ( path )
204+ . json ( )
205+ . catch ( ( err ) => {
206+ if ( err . code === "ENOENT" ) return { }
207+ throw new JsonError ( { path } , { cause : err } )
208+ } )
209+
210+ const parsed = Info . safeParse ( data )
211+ if ( parsed . success ) return parsed . data
212+ throw new InvalidError ( { path, issues : parsed . error . issues } )
213+ }
214+
215+ export const JsonError = NamedError . create (
216+ "ConfigJsonError" ,
217+ z . object ( {
218+ path : z . string ( ) ,
219+ } ) ,
220+ )
221+
222+ export const InvalidError = NamedError . create (
223+ "ConfigInvalidError" ,
224+ z . object ( {
225+ path : z . string ( ) ,
226+ issues : z . custom < z . ZodIssue [ ] > ( ) . optional ( ) ,
227+ } ) ,
228+ )
229+
166230 export function get ( ) {
167231 return state ( )
168232 }
0 commit comments