11/**
2- * JHT Config — IO utilities per ~/.jht/jht. config.json
2+ * JHT Config — IO utilities per ~/.jht/config.json
33 *
4- * Lettura, scrittura e validazione del file di configurazione.
4+ * Lettura (JSON5-compatibile), scrittura, validazione e hot reload.
5+ * Pattern copiato da OpenClaw (openclaw/src/config/io.ts).
56 */
67
78import * as fs from "node:fs" ;
@@ -17,11 +18,27 @@ export const JHT_CONFIG_DIR = path.join(os.homedir(), ".jht");
1718export const JHT_CONFIG_PATH = path . join ( JHT_CONFIG_DIR , "jht.config.json" ) ;
1819
1920/** Campi sensibili da mascherare nei log */
20- const SENSITIVE_FIELDS = [ "api_key" , "bot_token" , "session_token" ] ;
21+ const SENSITIVE_FIELDS = [ "api_key" , "bot_token" , "session_token" , "value" ] ;
22+
23+ // --- JSON5 leggero: strip commenti e trailing commas ---
24+
25+ /**
26+ * Parsa JSON5 leggero: rimuove commenti // e /* e trailing commas.
27+ * Non richiede dipendenze esterne.
28+ */
29+ export function parseJson5 ( raw : string ) : unknown {
30+ let cleaned = raw ;
31+ // Rimuovi commenti single-line (// ...) fuori da stringhe
32+ cleaned = cleaned . replace ( / ( " (?: [ ^ " \\ ] | \\ .) * " ) | \/ \/ [ ^ \n ] * / g, ( _ , str ) => str ?? "" ) ;
33+ // Rimuovi commenti multi-line (/* ... */)
34+ cleaned = cleaned . replace ( / ( " (?: [ ^ " \\ ] | \\ .) * " ) | \/ \* [ \s \S ] * ?\* \/ / g, ( _ , str ) => str ?? "" ) ;
35+ // Rimuovi trailing commas prima di } o ]
36+ cleaned = cleaned . replace ( / , ( \s * [ } \] ] ) / g, "$1" ) ;
37+ return JSON . parse ( cleaned ) ;
38+ }
2139
2240/**
23- * Legge e valida jht.config.json.
24- * Ritorna { success, data, error }.
41+ * Legge e valida il config. Supporta JSON e JSON5 (commenti, trailing commas).
2542 */
2643export function readConfig ( ) : {
2744 success : boolean ;
@@ -41,9 +58,9 @@ export function readConfig(): {
4158
4259 let parsed : unknown ;
4360 try {
44- parsed = JSON . parse ( raw ) ;
61+ parsed = parseJson5 ( raw ) ;
4562 } catch {
46- return { success : false , error : "JSON non valido in jht. config.json " } ;
63+ return { success : false , error : "JSON non valido in config" } ;
4764 }
4865
4966 const result = validateConfig ( parsed ) ;
@@ -56,8 +73,8 @@ export function readConfig(): {
5673}
5774
5875/**
59- * Scrive jht. config.json dopo validazione.
60- * Crea la directory ~/.jht/ se non esiste .
76+ * Scrive config dopo validazione. Crea ~/.jht/ se non esiste .
77+ * Scrive JSON formattato (non JSON5 — i commenti si perderebbero) .
6178 */
6279export function writeConfig ( config : unknown ) : {
6380 success : boolean ;
@@ -72,25 +89,28 @@ export function writeConfig(config: unknown): {
7289
7390 try {
7491 fs . mkdirSync ( JHT_CONFIG_DIR , { recursive : true } ) ;
75- fs . writeFileSync ( JHT_CONFIG_PATH , JSON . stringify ( result . data , null , 2 ) + "\n" , "utf-8" ) ;
92+ // Scrittura atomica: tmp + rename
93+ const tmpPath = JHT_CONFIG_PATH + ".tmp" ;
94+ fs . writeFileSync ( tmpPath , JSON . stringify ( result . data , null , 2 ) + "\n" , "utf-8" ) ;
95+ fs . renameSync ( tmpPath , JHT_CONFIG_PATH ) ;
7696 } catch ( err ) {
7797 return { success : false , error : `Errore scrittura file: ${ ( err as Error ) . message } ` } ;
7898 }
7999
100+ // Notifica listener hot reload
101+ for ( const cb of configChangeListeners ) {
102+ try { cb ( result . data ! ) ; } catch { /* ignora errori listener */ }
103+ }
104+
80105 return { success : true , data : result . data } ;
81106}
82107
83- /**
84- * Verifica se il file config esiste.
85- */
108+ /** Verifica se il file config esiste. */
86109export function configExists ( ) : boolean {
87110 return fs . existsSync ( JHT_CONFIG_PATH ) ;
88111}
89112
90- /**
91- * Ritorna una copia del config con i campi sensibili mascherati.
92- * Utile per logging e debug.
93- */
113+ /** Copia config con campi sensibili mascherati. */
94114export function redactConfig ( config : Record < string , unknown > ) : Record < string , unknown > {
95115 return JSON . parse ( JSON . stringify ( config ) , ( key , value ) => {
96116 if ( SENSITIVE_FIELDS . includes ( key ) && typeof value === "string" && value . length > 0 ) {
@@ -99,3 +119,41 @@ export function redactConfig(config: Record<string, unknown>): Record<string, un
99119 return value ;
100120 } ) ;
101121}
122+
123+ // --- Hot Reload ---
124+
125+ type ConfigChangeCallback = ( config : JHTConfigParsed ) => void ;
126+ const configChangeListeners : ConfigChangeCallback [ ] = [ ] ;
127+ let watcher : fs . FSWatcher | null = null ;
128+
129+ /** Registra un callback per hot reload del config. */
130+ export function onConfigChange ( callback : ConfigChangeCallback ) : ( ) => void {
131+ configChangeListeners . push ( callback ) ;
132+
133+ // Avvia watcher al primo listener
134+ if ( ! watcher && fs . existsSync ( JHT_CONFIG_PATH ) ) {
135+ let debounce : ReturnType < typeof setTimeout > | null = null ;
136+ watcher = fs . watch ( JHT_CONFIG_PATH , ( ) => {
137+ if ( debounce ) clearTimeout ( debounce ) ;
138+ debounce = setTimeout ( ( ) => {
139+ const result = readConfig ( ) ;
140+ if ( result . success && result . data ) {
141+ for ( const cb of configChangeListeners ) {
142+ try { cb ( result . data ) ; } catch { /* ignora */ }
143+ }
144+ }
145+ } , 200 ) ;
146+ } ) ;
147+ if ( watcher . unref ) watcher . unref ( ) ;
148+ }
149+
150+ // Ritorna funzione di unsubscribe
151+ return ( ) => {
152+ const idx = configChangeListeners . indexOf ( callback ) ;
153+ if ( idx >= 0 ) configChangeListeners . splice ( idx , 1 ) ;
154+ if ( configChangeListeners . length === 0 && watcher ) {
155+ watcher . close ( ) ;
156+ watcher = null ;
157+ }
158+ } ;
159+ }
0 commit comments