1- import { Effect , FileSystem , Path , Schema } from "effect" ;
1+ import { Effect , FileSystem , Path , Schema , SchemaTransformation } from "effect" ;
22import { parse , stringify } from "yaml" ;
3- import type { ProjectConfig , ProjectScope } from "../domain/project" ;
4- import { emptyProjectConfig } from "../domain/project" ;
5- import { ConfigError } from "../shared/errors" ;
6- import { getKeyDef } from "./keys" ;
3+ import type { ProjectConfig } from "../domain/project.ts " ;
4+ import { ProjectScope , emptyProjectConfig } from "../domain/project.ts " ;
5+ import { ConfigError } from "../shared/errors.ts " ;
6+ import { getKeyDef } from "./keys.ts " ;
77
8+ const TrimmedString = Schema . String . pipe ( Schema . decode ( SchemaTransformation . trim ( ) ) ) ;
9+ const NonEmptyTrimmedString = TrimmedString . check ( Schema . isNonEmpty ( ) ) ;
10+ const CompactTrimmedStringArray = Schema . Array ( TrimmedString ) . pipe (
11+ Schema . decodeTo (
12+ Schema . Array ( TrimmedString ) ,
13+ SchemaTransformation . transform ( {
14+ decode : ( items ) => items . filter ( ( item ) => item . length > 0 ) as ReadonlyArray < string > ,
15+ encode : ( items ) => items ,
16+ } ) ,
17+ ) ,
18+ ) ;
19+ const NonEmptyCompactTrimmedStringArray = CompactTrimmedStringArray . pipe (
20+ Schema . decodeTo (
21+ Schema . Array ( NonEmptyTrimmedString ) . check ( Schema . isNonEmpty ( ) ) ,
22+ SchemaTransformation . transform ( {
23+ decode : ( items ) => items ,
24+ encode : ( items ) => items ,
25+ } ) ,
26+ ) ,
27+ ) ;
28+
29+ const RawYamlMapSchema = Schema . Record ( Schema . String , Schema . Unknown ) ;
830type RawYamlMap = Record < string , unknown > ;
9- type RawScopeInput = string | { readonly name : string ; readonly description ?: string | undefined } ;
10- type RawHookInput = string | ReadonlyArray < string > ;
1131
12- const RawScopeSchema = Schema . Union ( [
32+ const RawScopeInput = Schema . Union ( [
1333 Schema . String ,
1434 Schema . Struct ( {
1535 name : Schema . String ,
16- description : Schema . optional ( Schema . String ) ,
36+ description : Schema . optionalKey ( Schema . String ) ,
1737 } ) ,
1838] ) ;
19- const RawHooksSchema = Schema . Union ( [ Schema . String , Schema . Array ( Schema . String ) ] ) ;
39+ const ScopeListField = Schema . Array ( RawScopeInput ) . pipe (
40+ Schema . decodeTo (
41+ Schema . Array ( ProjectScope ) ,
42+ SchemaTransformation . transform ( {
43+ decode : ( scopes ) =>
44+ scopes . map ( ( scope ) =>
45+ typeof scope === "string"
46+ ? { name : scope }
47+ : {
48+ name : scope . name ,
49+ ...( scope . description ?. trim ( ) . length ? { description : scope . description } : { } ) ,
50+ } ,
51+ ) as ReadonlyArray < { readonly name : string ; readonly description ?: string } > ,
52+ encode : ( scopes ) =>
53+ scopes . map ( ( scope ) => ( {
54+ name : scope . name ,
55+ ...( scope . description != null ? { description : scope . description } : { } ) ,
56+ } ) ) as ReadonlyArray < { readonly name : string ; readonly description ?: string } > ,
57+ } ) ,
58+ ) ,
59+ ) ;
60+
61+ const RawHookInput = Schema . Union ( [ Schema . String , Schema . Array ( Schema . String ) ] ) ;
62+ const HookListField = RawHookInput . pipe (
63+ Schema . decodeTo (
64+ NonEmptyCompactTrimmedStringArray ,
65+ SchemaTransformation . transform ( {
66+ decode : ( input ) =>
67+ ( typeof input === "string" ? input . split ( "," ) : [ ...input ] ) as ReadonlyArray < string > ,
68+ encode : ( input ) => input ,
69+ } ) ,
70+ ) ,
71+ ) ;
2072
21- const configError = ( pathValue : string , message : string ) =>
73+ const configError = ( pathValue : string , message : string , cause ?: unknown | undefined ) =>
2274 new ConfigError ( {
2375 message : `invalid config ${ pathValue } : ${ message } ` ,
76+ cause,
2477 } ) ;
2578
2679const decodeConfigField = < S extends Schema . Top > (
@@ -32,107 +85,62 @@ const decodeConfigField = <S extends Schema.Top>(
3285 input === undefined
3386 ? Effect . succeed ( undefined )
3487 : Schema . decodeUnknownEffect ( schema ) ( input ) . pipe (
35- Effect . mapError ( ( cause ) => configError ( pathValue , ` ${ key } : ${ cause . message } ` ) ) ,
88+ Effect . mapError ( ( cause ) => configError ( pathValue , key , cause ) ) ,
3689 ) ;
3790
3891const readYamlMap = Effect . fn ( function * ( pathValue : string ) {
3992 const fs = yield * FileSystem . FileSystem ;
4093 const exists = yield * fs . exists ( pathValue ) ;
4194 if ( ! exists ) {
42- return { } ;
95+ return { } as RawYamlMap ;
4396 }
4497
4598 const text = yield * fs . readFileString ( pathValue , "utf8" ) . pipe (
4699 Effect . mapError (
47100 ( cause ) =>
48101 new ConfigError ( {
49- message : `failed to read config ${ pathValue } : ${ cause . message } ` ,
102+ message : `failed to read config ${ pathValue } ` ,
103+ cause,
50104 } ) ,
51105 ) ,
52106 ) ;
53107
54108 return yield * Effect . try ( {
55- try : ( ) => {
56- const parsed = parse ( text ) ;
57- if ( typeof parsed !== "object" || parsed === null || Array . isArray ( parsed ) ) {
58- throw configError ( pathValue , "expected a YAML mapping" ) ;
59- }
60- return parsed as RawYamlMap ;
61- } ,
109+ try : ( ) => parse ( text ) ,
62110 catch : ( cause ) =>
63- ConfigError . is ( cause )
64- ? cause
65- : new ConfigError ( {
66- message : `failed to read config ${ pathValue } : ${ cause instanceof Error ? cause . message : String ( cause ) } ` ,
67- } ) ,
68- } ) ;
111+ new ConfigError ( {
112+ message : `failed to read config ${ pathValue } ` ,
113+ cause,
114+ } ) ,
115+ } ) . pipe (
116+ Effect . flatMap ( ( parsed ) => Schema . decodeUnknownEffect ( RawYamlMapSchema ) ( parsed ) ) ,
117+ Effect . map ( ( parsed ) => ( { ...parsed } ) ) ,
118+ Effect . mapError ( ( cause ) => configError ( pathValue , "expected a YAML mapping" , cause ) ) ,
119+ ) ;
69120} ) ;
70121
71- const normalizeScope = (
72- pathValue : string ,
73- input : RawScopeInput ,
74- index : number ,
75- ) : Effect . Effect < ProjectScope , ConfigError > => {
76- if ( typeof input === "string" ) {
77- const name = input . trim ( ) ;
78- return name . length > 0
79- ? Effect . succeed ( { name } )
80- : Effect . failSync ( ( ) => configError ( pathValue , `scopes[${ index } ] must not be empty` ) ) ;
81- }
82-
83- const name = input . name . trim ( ) ;
84- if ( name . length === 0 ) {
85- return Effect . failSync ( ( ) => configError ( pathValue , `scopes[${ index } ].name must not be empty` ) ) ;
86- }
87-
88- const description = input . description ?. trim ( ) ;
89- return Effect . succeed ( {
90- name,
91- ...( description != null && description . length > 0 ? { description } : { } ) ,
92- } ) ;
93- } ;
94-
95122const decodeScopes = Effect . fn ( function * ( pathValue : string , rawMap : RawYamlMap ) {
96- const scopes = yield * decodeConfigField (
97- pathValue ,
98- "scopes" ,
99- rawMap [ "scopes" ] ,
100- Schema . Array ( RawScopeSchema ) ,
101- ) ;
102- if ( scopes == null ) {
103- return [ ] as Array < ProjectScope > ;
104- }
105- return yield * Effect . forEach ( scopes , ( scope , index ) => normalizeScope ( pathValue , scope , index ) ) ;
123+ return ( ( yield * decodeConfigField ( pathValue , "scopes" , rawMap [ "scopes" ] , ScopeListField ) ) ??
124+ [ ] ) as Array < ProjectScope > ;
106125} ) ;
107126
108- const normalizeHookValues = (
109- pathValue : string ,
110- key : "hook" | "hook_type" ,
111- input : RawHookInput ,
112- ) : Effect . Effect < Array < string > , ConfigError > => {
113- const normalized = ( typeof input === "string" ? input . split ( "," ) : [ ...input ] )
114- . map ( ( item ) => item . trim ( ) )
115- . filter ( ( item ) => item . length > 0 ) ;
116- return normalized . length > 0
117- ? Effect . succeed ( normalized )
118- : Effect . failSync ( ( ) => configError ( pathValue , `${ key } must not be empty` ) ) ;
119- } ;
120-
121127const decodeHooks = Effect . fn ( function * ( pathValue : string , rawMap : RawYamlMap ) {
122- const hooks = yield * decodeConfigField ( pathValue , "hook" , rawMap [ "hook" ] , RawHooksSchema ) ;
128+ const hooks = yield * decodeConfigField ( pathValue , "hook" , rawMap [ "hook" ] , HookListField ) ;
123129 if ( hooks != null ) {
124- return yield * normalizeHookValues ( pathValue , "hook" , hooks ) ;
130+ return hooks ;
125131 }
126132
127133 const legacyHook = yield * decodeConfigField (
128134 pathValue ,
129135 "hook_type" ,
130136 rawMap [ "hook_type" ] ,
131- Schema . String ,
137+ HookListField ,
132138 ) ;
139+
133140 if ( legacyHook != null ) {
134- return yield * normalizeHookValues ( pathValue , "hook_type" , legacyHook ) ;
141+ return legacyHook ;
135142 }
143+
136144 return [ ] as Array < string > ;
137145} ) ;
138146
@@ -158,7 +166,9 @@ export const projectConfigWritePath = (repoRoot: string) => gitAgentPath(repoRoo
158166
159167export const localConfigPath = ( repoRoot : string ) => gitAgentPath ( repoRoot , "config.local.yml" ) ;
160168
161- export const loadProjectConfig = Effect . fn ( function * ( repoRoot : string ) {
169+ export const loadProjectConfig = Effect . fn ( "Config.LoadProjectConfig" ) ( function * (
170+ repoRoot : string ,
171+ ) {
162172 const projectPath = yield * projectConfigPath ( repoRoot ) ;
163173 const localPath = yield * localConfigPath ( repoRoot ) ;
164174 const projectRaw = yield * readYamlMap ( projectPath ) ;
@@ -235,7 +245,7 @@ export const loadProjectConfig = Effect.fn(function* (repoRoot: string) {
235245 } satisfies ProjectConfig ;
236246} ) ;
237247
238- export const mergeAndSaveScopes = Effect . fn ( function * (
248+ export const mergeScopes = Effect . fn ( "Config.MergeScopes" ) ( function * (
239249 pathValue : string ,
240250 nextScopes : ReadonlyArray < ProjectScope > ,
241251) {
@@ -268,19 +278,23 @@ export const mergeAndSaveScopes = Effect.fn(function* (
268278 }
269279
270280 rawMap [ "scopes" ] = merged ;
281+
271282 yield * fs . makeDirectory ( path . dirname ( pathValue ) , { recursive : true } ) . pipe (
272283 Effect . mapError (
273284 ( cause ) =>
274285 new ConfigError ( {
275- message : `failed to save scopes to ${ pathValue } : ${ cause . message } ` ,
286+ message : `failed to save scopes to ${ pathValue } ` ,
287+ cause,
276288 } ) ,
277289 ) ,
278290 ) ;
291+
279292 yield * fs . writeFileString ( pathValue , stringify ( rawMap ) , { mode : 0o644 } ) . pipe (
280293 Effect . mapError (
281294 ( cause ) =>
282295 new ConfigError ( {
283- message : `failed to save scopes to ${ pathValue } : ${ cause . message } ` ,
296+ message : `failed to save scopes to ${ pathValue } ` ,
297+ cause,
284298 } ) ,
285299 ) ,
286300 ) ;
@@ -307,14 +321,16 @@ export const readProjectField = (pathValue: string, key: string) =>
307321 return yamlValueToString ( rawMap [ key ] ) ;
308322 } ) ;
309323
310- export const writeProjectField = Effect . fn ( function * (
324+ export const writeProjectField = Effect . fn ( "Config.WriteProjectField" ) ( function * (
311325 pathValue : string ,
312326 key : string ,
313327 value : string ,
314328) {
315329 const fs = yield * FileSystem . FileSystem ;
316330 const path = yield * Path . Path ;
331+
317332 const rawMap = yield * readYamlMap ( pathValue ) ;
333+
318334 const def = getKeyDef ( key ) ;
319335 if ( def == null ) {
320336 return yield * new ConfigError ( { message : `unknown config key "${ key } "` } ) ;
@@ -341,15 +357,18 @@ export const writeProjectField = Effect.fn(function* (
341357 Effect . mapError (
342358 ( cause ) =>
343359 new ConfigError ( {
344- message : `failed to write config ${ pathValue } : ${ cause . message } ` ,
360+ message : `failed to write config ${ pathValue } ` ,
361+ cause,
345362 } ) ,
346363 ) ,
347364 ) ;
365+
348366 yield * fs . writeFileString ( pathValue , stringify ( rawMap ) , { mode : 0o644 } ) . pipe (
349367 Effect . mapError (
350368 ( cause ) =>
351369 new ConfigError ( {
352- message : `failed to write config ${ pathValue } : ${ cause . message } ` ,
370+ message : `failed to write config ${ pathValue } ` ,
371+ cause,
353372 } ) ,
354373 ) ,
355374 ) ;
0 commit comments