odac 0.9.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 (213) hide show
  1. package/.editorconfig +21 -0
  2. package/.github/workflows/auto-pr-description.yml +49 -0
  3. package/.github/workflows/release.yml +32 -0
  4. package/.github/workflows/test-coverage.yml +58 -0
  5. package/.husky/pre-commit +2 -0
  6. package/.kiro/steering/code-style.md +56 -0
  7. package/.kiro/steering/product.md +20 -0
  8. package/.kiro/steering/structure.md +77 -0
  9. package/.kiro/steering/tech.md +87 -0
  10. package/.prettierrc +10 -0
  11. package/.releaserc.js +134 -0
  12. package/AGENTS.md +84 -0
  13. package/CHANGELOG.md +181 -0
  14. package/CODE_OF_CONDUCT.md +83 -0
  15. package/CONTRIBUTING.md +63 -0
  16. package/LICENSE +661 -0
  17. package/README.md +57 -0
  18. package/SECURITY.md +26 -0
  19. package/bin/candy +10 -0
  20. package/bin/candypack +10 -0
  21. package/cli/index.js +3 -0
  22. package/cli/src/Cli.js +348 -0
  23. package/cli/src/Connector.js +93 -0
  24. package/cli/src/Monitor.js +416 -0
  25. package/core/Candy.js +87 -0
  26. package/core/Commands.js +239 -0
  27. package/core/Config.js +1094 -0
  28. package/core/Lang.js +52 -0
  29. package/core/Log.js +43 -0
  30. package/core/Process.js +26 -0
  31. package/docs/backend/01-overview/01-whats-in-the-candy-box.md +9 -0
  32. package/docs/backend/01-overview/02-super-handy-helper-functions.md +9 -0
  33. package/docs/backend/01-overview/03-development-server.md +79 -0
  34. package/docs/backend/02-structure/01-typical-project-layout.md +39 -0
  35. package/docs/backend/03-config/00-configuration-overview.md +214 -0
  36. package/docs/backend/03-config/01-database-connection.md +60 -0
  37. package/docs/backend/03-config/02-static-route-mapping-optional.md +20 -0
  38. package/docs/backend/03-config/03-request-timeout.md +11 -0
  39. package/docs/backend/03-config/04-environment-variables.md +227 -0
  40. package/docs/backend/03-config/05-early-hints.md +352 -0
  41. package/docs/backend/04-routing/01-basic-page-routes.md +28 -0
  42. package/docs/backend/04-routing/02-controller-less-view-routes.md +43 -0
  43. package/docs/backend/04-routing/03-api-and-data-routes.md +20 -0
  44. package/docs/backend/04-routing/04-authentication-aware-routes.md +48 -0
  45. package/docs/backend/04-routing/05-advanced-routing.md +14 -0
  46. package/docs/backend/04-routing/06-error-pages.md +101 -0
  47. package/docs/backend/04-routing/07-cron-jobs.md +149 -0
  48. package/docs/backend/05-controllers/01-how-to-build-a-controller.md +17 -0
  49. package/docs/backend/05-controllers/02-your-trusty-candy-assistant.md +20 -0
  50. package/docs/backend/05-controllers/03-controller-classes.md +93 -0
  51. package/docs/backend/05-forms/01-custom-forms.md +395 -0
  52. package/docs/backend/05-forms/02-automatic-database-insert.md +297 -0
  53. package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +96 -0
  54. package/docs/backend/06-request-and-response/02-sending-a-response-replying-to-the-user.md +40 -0
  55. package/docs/backend/07-views/01-the-view-directory.md +73 -0
  56. package/docs/backend/07-views/02-rendering-a-view.md +179 -0
  57. package/docs/backend/07-views/03-template-syntax.md +181 -0
  58. package/docs/backend/07-views/03-variables.md +328 -0
  59. package/docs/backend/07-views/04-request-data.md +231 -0
  60. package/docs/backend/07-views/05-conditionals.md +290 -0
  61. package/docs/backend/07-views/06-loops.md +353 -0
  62. package/docs/backend/07-views/07-translations.md +358 -0
  63. package/docs/backend/07-views/08-backend-javascript.md +398 -0
  64. package/docs/backend/07-views/09-comments.md +297 -0
  65. package/docs/backend/08-database/01-database-connection.md +99 -0
  66. package/docs/backend/08-database/02-using-mysql.md +322 -0
  67. package/docs/backend/09-validation/01-the-validator-service.md +424 -0
  68. package/docs/backend/10-authentication/01-user-logins-with-authjs.md +53 -0
  69. package/docs/backend/10-authentication/02-foiling-villains-with-csrf-protection.md +55 -0
  70. package/docs/backend/10-authentication/03-register.md +134 -0
  71. package/docs/backend/10-authentication/04-candy-register-forms.md +676 -0
  72. package/docs/backend/10-authentication/05-session-management.md +159 -0
  73. package/docs/backend/10-authentication/06-candy-login-forms.md +596 -0
  74. package/docs/backend/11-mail/01-the-mail-service.md +42 -0
  75. package/docs/backend/12-streaming/01-streaming-overview.md +300 -0
  76. package/docs/backend/13-utilities/01-candy-var.md +504 -0
  77. package/docs/frontend/01-overview/01-introduction.md +146 -0
  78. package/docs/frontend/02-ajax-navigation/01-quick-start.md +608 -0
  79. package/docs/frontend/02-ajax-navigation/02-configuration.md +370 -0
  80. package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +519 -0
  81. package/docs/frontend/03-forms/01-form-handling.md +420 -0
  82. package/docs/frontend/04-api-requests/01-get-post.md +443 -0
  83. package/docs/frontend/05-streaming/01-client-streaming.md +163 -0
  84. package/docs/index.json +452 -0
  85. package/docs/server/01-installation/01-quick-install.md +19 -0
  86. package/docs/server/01-installation/02-manual-installation-via-npm.md +9 -0
  87. package/docs/server/02-get-started/01-core-concepts.md +7 -0
  88. package/docs/server/02-get-started/02-basic-commands.md +57 -0
  89. package/docs/server/02-get-started/03-cli-reference.md +276 -0
  90. package/docs/server/02-get-started/04-cli-quick-reference.md +102 -0
  91. package/docs/server/03-service/01-start-a-new-service.md +57 -0
  92. package/docs/server/03-service/02-delete-a-service.md +48 -0
  93. package/docs/server/04-web/01-create-a-website.md +36 -0
  94. package/docs/server/04-web/02-list-websites.md +9 -0
  95. package/docs/server/04-web/03-delete-a-website.md +29 -0
  96. package/docs/server/05-subdomain/01-create-a-subdomain.md +32 -0
  97. package/docs/server/05-subdomain/02-list-subdomains.md +33 -0
  98. package/docs/server/05-subdomain/03-delete-a-subdomain.md +41 -0
  99. package/docs/server/06-ssl/01-renew-an-ssl-certificate.md +34 -0
  100. package/docs/server/07-mail/01-create-a-mail-account.md +23 -0
  101. package/docs/server/07-mail/02-delete-a-mail-account.md +20 -0
  102. package/docs/server/07-mail/03-list-mail-accounts.md +20 -0
  103. package/docs/server/07-mail/04-change-account-password.md +23 -0
  104. package/eslint.config.mjs +120 -0
  105. package/framework/index.js +4 -0
  106. package/framework/src/Auth.js +309 -0
  107. package/framework/src/Candy.js +81 -0
  108. package/framework/src/Config.js +79 -0
  109. package/framework/src/Env.js +60 -0
  110. package/framework/src/Lang.js +57 -0
  111. package/framework/src/Mail.js +83 -0
  112. package/framework/src/Mysql.js +575 -0
  113. package/framework/src/Request.js +301 -0
  114. package/framework/src/Route/Cron.js +128 -0
  115. package/framework/src/Route/Internal.js +439 -0
  116. package/framework/src/Route.js +455 -0
  117. package/framework/src/Server.js +15 -0
  118. package/framework/src/Stream.js +163 -0
  119. package/framework/src/Token.js +37 -0
  120. package/framework/src/Validator.js +271 -0
  121. package/framework/src/Var.js +211 -0
  122. package/framework/src/View/EarlyHints.js +190 -0
  123. package/framework/src/View/Form.js +600 -0
  124. package/framework/src/View.js +513 -0
  125. package/framework/web/candy.js +838 -0
  126. package/jest.config.js +22 -0
  127. package/locale/de-DE.json +80 -0
  128. package/locale/en-US.json +79 -0
  129. package/locale/es-ES.json +80 -0
  130. package/locale/fr-FR.json +80 -0
  131. package/locale/pt-BR.json +80 -0
  132. package/locale/ru-RU.json +80 -0
  133. package/locale/tr-TR.json +85 -0
  134. package/locale/zh-CN.json +80 -0
  135. package/package.json +86 -0
  136. package/server/index.js +5 -0
  137. package/server/src/Api.js +88 -0
  138. package/server/src/DNS.js +940 -0
  139. package/server/src/Hub.js +535 -0
  140. package/server/src/Mail.js +571 -0
  141. package/server/src/SSL.js +180 -0
  142. package/server/src/Server.js +27 -0
  143. package/server/src/Service.js +248 -0
  144. package/server/src/Subdomain.js +64 -0
  145. package/server/src/Web/Firewall.js +170 -0
  146. package/server/src/Web/Proxy.js +134 -0
  147. package/server/src/Web.js +451 -0
  148. package/server/src/mail/imap.js +1091 -0
  149. package/server/src/mail/server.js +32 -0
  150. package/server/src/mail/smtp.js +786 -0
  151. package/test/cli/Cli.test.js +36 -0
  152. package/test/core/Candy.test.js +234 -0
  153. package/test/core/Commands.test.js +538 -0
  154. package/test/core/Config.test.js +1435 -0
  155. package/test/core/Lang.test.js +250 -0
  156. package/test/core/Process.test.js +156 -0
  157. package/test/framework/Route.test.js +239 -0
  158. package/test/framework/View/EarlyHints.test.js +282 -0
  159. package/test/scripts/check-coverage.js +132 -0
  160. package/test/server/Api.test.js +647 -0
  161. package/test/server/Client.test.js +338 -0
  162. package/test/server/DNS.test.js +2050 -0
  163. package/test/server/DNS.test.js.bak +2084 -0
  164. package/test/server/Log.test.js +73 -0
  165. package/test/server/Mail.account.test_.js +460 -0
  166. package/test/server/Mail.init.test_.js +411 -0
  167. package/test/server/Mail.test_.js +1340 -0
  168. package/test/server/SSL.test_.js +1491 -0
  169. package/test/server/Server.test.js +765 -0
  170. package/test/server/Service.test_.js +1127 -0
  171. package/test/server/Subdomain.test.js +440 -0
  172. package/test/server/Web/Firewall.test.js +175 -0
  173. package/test/server/Web.test_.js +1562 -0
  174. package/test/server/__mocks__/acme-client.js +17 -0
  175. package/test/server/__mocks__/bcrypt.js +50 -0
  176. package/test/server/__mocks__/child_process.js +389 -0
  177. package/test/server/__mocks__/crypto.js +432 -0
  178. package/test/server/__mocks__/fs.js +450 -0
  179. package/test/server/__mocks__/globalCandy.js +227 -0
  180. package/test/server/__mocks__/http-proxy.js +105 -0
  181. package/test/server/__mocks__/http.js +575 -0
  182. package/test/server/__mocks__/https.js +272 -0
  183. package/test/server/__mocks__/index.js +249 -0
  184. package/test/server/__mocks__/mail/server.js +100 -0
  185. package/test/server/__mocks__/mail/smtp.js +31 -0
  186. package/test/server/__mocks__/mailparser.js +81 -0
  187. package/test/server/__mocks__/net.js +369 -0
  188. package/test/server/__mocks__/node-forge.js +328 -0
  189. package/test/server/__mocks__/os.js +320 -0
  190. package/test/server/__mocks__/path.js +291 -0
  191. package/test/server/__mocks__/selfsigned.js +8 -0
  192. package/test/server/__mocks__/server/src/mail/server.js +100 -0
  193. package/test/server/__mocks__/server/src/mail/smtp.js +31 -0
  194. package/test/server/__mocks__/smtp-server.js +106 -0
  195. package/test/server/__mocks__/sqlite3.js +394 -0
  196. package/test/server/__mocks__/testFactories.js +299 -0
  197. package/test/server/__mocks__/testHelpers.js +363 -0
  198. package/test/server/__mocks__/tls.js +229 -0
  199. package/watchdog/index.js +3 -0
  200. package/watchdog/src/Watchdog.js +156 -0
  201. package/web/config.json +5 -0
  202. package/web/controller/page/about.js +27 -0
  203. package/web/controller/page/index.js +34 -0
  204. package/web/package.json +18 -0
  205. package/web/public/assets/css/style.css +1835 -0
  206. package/web/public/assets/js/app.js +96 -0
  207. package/web/route/www.js +19 -0
  208. package/web/skeleton/main.html +22 -0
  209. package/web/view/content/about.html +65 -0
  210. package/web/view/content/home.html +205 -0
  211. package/web/view/footer/main.html +11 -0
  212. package/web/view/head/main.html +5 -0
  213. package/web/view/header/main.html +14 -0
package/core/Config.js ADDED
@@ -0,0 +1,1094 @@
1
+ const fs = require('fs')
2
+ const os = require('os')
3
+ const path = require('path')
4
+
5
+ const {log, error} = Candy.core('Log', false).init('Config')
6
+
7
+ class Config {
8
+ #dir
9
+ #file
10
+ #configDir
11
+ #loaded = false
12
+ #saving = false
13
+ #changed = false
14
+ #moduleChanged = {}
15
+ #isModular = false
16
+ config = {
17
+ server: {
18
+ pid: null,
19
+ started: null,
20
+ watchdog: null
21
+ }
22
+ }
23
+
24
+ // Module mapping configuration - defines which config keys belong to which module files
25
+ #moduleMap = {
26
+ server: ['server'],
27
+ web: ['websites', 'web'],
28
+ service: ['services'],
29
+ ssl: ['ssl'],
30
+ mail: ['mail'],
31
+ dns: ['dns'],
32
+ api: ['api'],
33
+ firewall: ['firewall'],
34
+ hub: ['hub']
35
+ }
36
+
37
+ // Initialize default configuration for module keys
38
+ #initializeDefaultModuleConfig(config, keys) {
39
+ for (const key of keys) {
40
+ if (config[key] === undefined) {
41
+ // Initialize with appropriate default values
42
+ if (key === 'server') {
43
+ config[key] = {
44
+ pid: null,
45
+ started: null,
46
+ watchdog: null
47
+ }
48
+ } else if (key === 'websites') {
49
+ config[key] = {}
50
+ } else if (key === 'services') {
51
+ config[key] = []
52
+ } else if (key === 'firewall') {
53
+ config[key] = {
54
+ enabled: true,
55
+ blacklist: [],
56
+ whitelist: [],
57
+ rateLimit: {
58
+ enabled: true,
59
+ windowMs: 60000,
60
+ max: 300
61
+ }
62
+ }
63
+ } else {
64
+ config[key] = {}
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ force() {
71
+ this.#changed = true
72
+ // Mark all modules as changed to force save
73
+ for (const moduleName of Object.keys(this.#moduleMap)) {
74
+ this.#moduleChanged[moduleName] = true
75
+ }
76
+ this.#save()
77
+ }
78
+
79
+ // Detect configuration format: 'modular', 'single', or 'new'
80
+ #detectConfigFormat() {
81
+ const modularExists = fs.existsSync(this.#configDir)
82
+ const singleExists = fs.existsSync(this.#file)
83
+
84
+ if (modularExists) {
85
+ return 'modular'
86
+ } else if (singleExists) {
87
+ return 'single'
88
+ } else {
89
+ return 'new' // Fresh installation
90
+ }
91
+ }
92
+
93
+ // Get module name for a config key
94
+ #getModuleForKey(key) {
95
+ for (const [module, keys] of Object.entries(this.#moduleMap)) {
96
+ if (keys.includes(key)) return module
97
+ }
98
+ return null
99
+ }
100
+
101
+ // Load individual module file from config directory with corruption recovery
102
+ #loadModuleFile(moduleName) {
103
+ const moduleFile = path.join(this.#configDir, moduleName + '.json')
104
+ const bakDir = path.join(this.#dir, '.bak')
105
+ const backupFile = path.join(bakDir, moduleName + '.json.bak')
106
+ const corruptedFile = moduleFile + '.corrupted'
107
+
108
+ // Return null if file doesn't exist
109
+ if (!fs.existsSync(moduleFile)) {
110
+ return null
111
+ }
112
+
113
+ // Try to load the main file
114
+ try {
115
+ const data = fs.readFileSync(moduleFile, 'utf8')
116
+ if (!data || data.length < 2) {
117
+ error(`Module file ${moduleName}.json is empty`)
118
+ // Try backup if main file is empty
119
+ return this.#loadModuleFromBackup(moduleName, moduleFile, backupFile, corruptedFile)
120
+ }
121
+ return JSON.parse(data)
122
+ } catch (err) {
123
+ // JSON parse error or read error detected
124
+ error(`Error loading module file ${moduleName}.json:`, err.message)
125
+
126
+ // Try to recover from backup
127
+ return this.#loadModuleFromBackup(moduleName, moduleFile, backupFile, corruptedFile)
128
+ }
129
+ }
130
+
131
+ // Atomic write helper method - writes data safely with backup
132
+ #atomicWrite(filePath, data) {
133
+ const tempFile = filePath + '.tmp'
134
+ const bakDir = path.join(this.#dir, '.bak')
135
+ const fileName = path.basename(filePath)
136
+ const backupFile = path.join(bakDir, fileName + '.bak')
137
+
138
+ try {
139
+ // 1. Write to temporary file first
140
+ const jsonData = JSON.stringify(data, null, 4)
141
+ fs.writeFileSync(tempFile, jsonData, 'utf8')
142
+
143
+ // 2. Copy existing file to .bak directory before overwriting (if it exists)
144
+ if (fs.existsSync(filePath)) {
145
+ try {
146
+ // Ensure .bak directory exists
147
+ if (!fs.existsSync(bakDir)) {
148
+ fs.mkdirSync(bakDir, {recursive: true})
149
+ }
150
+ fs.copyFileSync(filePath, backupFile)
151
+ } catch (backupErr) {
152
+ error(`[Config] Warning: Failed to create backup for ${filePath}: ${backupErr.message}`)
153
+ // Continue anyway - better to save without backup than not save at all
154
+ }
155
+ }
156
+
157
+ // 3. Atomic rename to replace main file
158
+ fs.renameSync(tempFile, filePath)
159
+
160
+ return true
161
+ } catch (err) {
162
+ error(`[Config] Atomic write failed for ${filePath}: ${err.message}`)
163
+ error(`[Config] Error code: ${err.code}`)
164
+
165
+ // Clean up temporary file on error
166
+ if (fs.existsSync(tempFile)) {
167
+ try {
168
+ fs.unlinkSync(tempFile)
169
+ } catch (cleanupErr) {
170
+ error(`[Config] Failed to clean up temp file ${tempFile}: ${cleanupErr.message}`)
171
+ }
172
+ }
173
+ throw err
174
+ }
175
+ }
176
+
177
+ // Attempt to load module from backup file
178
+ #loadModuleFromBackup(moduleName, moduleFile, backupFile, corruptedFile) {
179
+ // Check if backup file exists
180
+ if (!fs.existsSync(backupFile)) {
181
+ error(`No backup file found for ${moduleName}.json, initializing with defaults`)
182
+ return null
183
+ }
184
+
185
+ try {
186
+ // Try to load from backup
187
+ const backupData = fs.readFileSync(backupFile, 'utf8')
188
+ if (!backupData || backupData.length < 2) {
189
+ error(`Backup file ${moduleName}.json.bak is empty, initializing with defaults`)
190
+ return null
191
+ }
192
+
193
+ const parsedData = JSON.parse(backupData)
194
+
195
+ // Backup is valid - create .corrupted backup of broken file
196
+ try {
197
+ if (fs.existsSync(moduleFile)) {
198
+ fs.copyFileSync(moduleFile, corruptedFile)
199
+ log(`Created corrupted backup: ${moduleName}.json.corrupted`)
200
+ }
201
+ } catch (copyErr) {
202
+ error(`Failed to create corrupted backup for ${moduleName}.json:`, copyErr.message)
203
+ }
204
+
205
+ // Restore from backup to main file
206
+ try {
207
+ fs.writeFileSync(moduleFile, backupData, 'utf8')
208
+ log(`Restored ${moduleName}.json from backup`)
209
+ } catch (writeErr) {
210
+ error(`Failed to restore ${moduleName}.json from backup:`, writeErr.message)
211
+ }
212
+
213
+ return parsedData
214
+ } catch (err) {
215
+ // Both main and backup are corrupted
216
+ error(`Both ${moduleName}.json and backup are corrupted:`, err.message)
217
+ error(`Initializing ${moduleName} with default values`)
218
+
219
+ // Create .corrupted backup of both files if they exist
220
+ try {
221
+ if (fs.existsSync(moduleFile)) {
222
+ fs.copyFileSync(moduleFile, corruptedFile)
223
+ }
224
+ if (fs.existsSync(backupFile)) {
225
+ fs.copyFileSync(backupFile, backupFile + '.corrupted')
226
+ }
227
+ } catch (copyErr) {
228
+ error(`Failed to create corrupted backups:`, copyErr.message)
229
+ }
230
+
231
+ return null
232
+ }
233
+ }
234
+
235
+ // Merge all module configs into single in-memory object
236
+ #loadModular() {
237
+ log('[Config] Loading modular configuration...')
238
+
239
+ try {
240
+ // Start with default config structure
241
+ const mergedConfig = {
242
+ server: {
243
+ pid: null,
244
+ started: null,
245
+ watchdog: null
246
+ }
247
+ }
248
+
249
+ let loadedModules = 0
250
+ let failedModules = []
251
+
252
+ // Iterate through all modules and merge their data
253
+ for (const [moduleName, keys] of Object.entries(this.#moduleMap)) {
254
+ try {
255
+ const moduleData = this.#loadModuleFile(moduleName)
256
+
257
+ if (moduleData && typeof moduleData === 'object') {
258
+ // Merge each key from the module into the main config
259
+ for (const key of keys) {
260
+ if (moduleData[key] !== undefined) {
261
+ mergedConfig[key] = moduleData[key]
262
+ }
263
+ }
264
+ loadedModules++
265
+ } else {
266
+ // Module file is missing or corrupted - initialize with defaults
267
+ log(`[Config] Module ${moduleName} not loaded, using defaults`)
268
+ this.#initializeDefaultModuleConfig(mergedConfig, keys)
269
+ }
270
+ } catch (err) {
271
+ error(`[Config] Error loading module ${moduleName}: ${err.message}`)
272
+ failedModules.push(moduleName)
273
+
274
+ // Initialize with defaults even on error
275
+ this.#initializeDefaultModuleConfig(mergedConfig, keys)
276
+ }
277
+ }
278
+
279
+ this.config = mergedConfig
280
+ this.#loaded = true
281
+
282
+ log(`[Config] Loaded ${loadedModules} module(s) successfully`)
283
+ if (failedModules.length > 0) {
284
+ log(`[Config] Failed to load ${failedModules.length} module(s): ${failedModules.join(', ')}`)
285
+ }
286
+ } catch (err) {
287
+ error(`[Config] Critical error during modular load: ${err.message}`)
288
+ error(`[Config] Error code: ${err.code}`)
289
+ error('[Config] Attempting to fall back to single-file mode')
290
+
291
+ // Try to fall back to single-file if it exists
292
+ if (fs.existsSync(this.#file)) {
293
+ log('[Config] Single-file config found, attempting to load')
294
+ this.#isModular = false
295
+ this.#load()
296
+ } else {
297
+ error('[Config] No fallback available, using default configuration')
298
+ this.config = {
299
+ server: {
300
+ pid: null,
301
+ started: null,
302
+ watchdog: null
303
+ }
304
+ }
305
+ this.#loaded = true
306
+ }
307
+ }
308
+ }
309
+
310
+ // Migrate from single-file to modular format
311
+ #migrate() {
312
+ log('[Config] Starting migration from single-file to modular configuration...')
313
+
314
+ try {
315
+ // 1. Create config directory if it doesn't exist
316
+ if (!fs.existsSync(this.#configDir)) {
317
+ try {
318
+ fs.mkdirSync(this.#configDir, {recursive: true})
319
+ log(`[Config] Created config directory: ${this.#configDir}`)
320
+ } catch (mkdirErr) {
321
+ error(`[Config] Failed to create config directory: ${mkdirErr.message}`)
322
+ error(`[Config] Error code: ${mkdirErr.code}`)
323
+ if (mkdirErr.code === 'EACCES' || mkdirErr.code === 'EPERM') {
324
+ error('[Config] Permission denied - check file system permissions')
325
+ }
326
+ throw mkdirErr
327
+ }
328
+ }
329
+
330
+ // 2. Create backup of original config.json as config.json.pre-modular
331
+ const preModularBackup = this.#file + '.pre-modular'
332
+ if (fs.existsSync(this.#file) && !fs.existsSync(preModularBackup)) {
333
+ try {
334
+ fs.copyFileSync(this.#file, preModularBackup)
335
+ log(`[Config] Created pre-migration backup: ${preModularBackup}`)
336
+ } catch (backupErr) {
337
+ error(`[Config] Failed to create pre-migration backup: ${backupErr.message}`)
338
+ error(`[Config] Error code: ${backupErr.code}`)
339
+ // Continue anyway - migration can proceed without backup
340
+ log('[Config] Continuing migration without backup')
341
+ }
342
+ }
343
+
344
+ // 3. Store original config for verification (only keys that actually exist)
345
+ const originalConfig = JSON.parse(JSON.stringify(this.config))
346
+ log(`[Config] Original config keys: ${Object.keys(originalConfig).join(', ')}`)
347
+
348
+ // 4. Split config object by module mapping and write each module
349
+ let successfulWrites = 0
350
+ let failedWrites = []
351
+
352
+ for (const [moduleName, keys] of Object.entries(this.#moduleMap)) {
353
+ const moduleData = {}
354
+ let hasData = false
355
+
356
+ // Extract relevant config keys for this module
357
+ for (const key of keys) {
358
+ if (this.config[key] !== undefined) {
359
+ moduleData[key] = this.config[key]
360
+ hasData = true
361
+ }
362
+ }
363
+
364
+ // Write module file if it has any keys (even if empty objects/arrays)
365
+ if (hasData) {
366
+ const moduleFile = path.join(this.#configDir, moduleName + '.json')
367
+
368
+ try {
369
+ // Write each module to its respective file using atomic write
370
+ this.#atomicWrite(moduleFile, moduleData)
371
+ log(`[Config] Created module file: ${moduleName}.json`)
372
+ successfulWrites++
373
+ } catch (err) {
374
+ error(`[Config] Failed to write module ${moduleName}.json: ${err.message}`)
375
+ error(`[Config] Error code: ${err.code}`)
376
+ failedWrites.push(moduleName)
377
+
378
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
379
+ error(`[Config] Permission denied for ${moduleFile}`)
380
+ } else if (err.code === 'ENOSPC') {
381
+ error('[Config] No space left on device')
382
+ }
383
+
384
+ throw new Error(`Migration failed while writing ${moduleName}.json: ${err.message}`)
385
+ }
386
+ }
387
+ }
388
+
389
+ log(`[Config] Successfully wrote ${successfulWrites} module file(s)`)
390
+
391
+ // 5. Verify migration
392
+ const verificationResult = this.#verifyMigration(originalConfig)
393
+
394
+ if (verificationResult.success) {
395
+ log('[Config] Migration completed successfully')
396
+ log('[Config] Data integrity verified')
397
+ this.#isModular = true
398
+ return true
399
+ } else {
400
+ error(`[Config] Migration verification failed: ${verificationResult.error}`)
401
+ throw new Error(`Migration verification failed: ${verificationResult.error}`)
402
+ }
403
+ } catch (err) {
404
+ error(`[Config] Migration failed: ${err.message}`)
405
+ error(`[Config] Error code: ${err.code}`)
406
+ error('[Config] System will remain in single-file mode')
407
+ // Rollback will be handled by the caller
408
+ return false
409
+ }
410
+ }
411
+
412
+ // Verify migration by loading modular config and comparing with original
413
+ #verifyMigration(originalConfig) {
414
+ try {
415
+ log('Verifying migration data integrity...')
416
+
417
+ // Load modular config back into memory
418
+ const verifyConfig = {
419
+ server: {
420
+ pid: null,
421
+ started: null,
422
+ watchdog: null
423
+ }
424
+ }
425
+
426
+ // Load all module files
427
+ for (const [moduleName, keys] of Object.entries(this.#moduleMap)) {
428
+ const moduleData = this.#loadModuleFile(moduleName)
429
+
430
+ if (moduleData && typeof moduleData === 'object') {
431
+ for (const key of keys) {
432
+ if (moduleData[key] !== undefined) {
433
+ verifyConfig[key] = moduleData[key]
434
+ }
435
+ }
436
+ }
437
+ }
438
+
439
+ // Only verify keys that exist in original config
440
+ // Missing keys in both configs are acceptable (e.g., DNS not configured yet)
441
+ for (const key of Object.keys(originalConfig)) {
442
+ if (!(key in verifyConfig)) {
443
+ return {
444
+ success: false,
445
+ error: `Data mismatch: ${key} exists in original but missing after migration`
446
+ }
447
+ }
448
+
449
+ const comparison = this.#deepCompare(originalConfig[key], verifyConfig[key], key)
450
+ if (!comparison.equal) {
451
+ return {
452
+ success: false,
453
+ error: `Data mismatch in ${key}: ${comparison.differences.join(', ')}`
454
+ }
455
+ }
456
+ }
457
+
458
+ log('Migration verification passed - data integrity confirmed')
459
+ return {success: true}
460
+ } catch (err) {
461
+ return {
462
+ success: false,
463
+ error: `Verification error: ${err.message}`
464
+ }
465
+ }
466
+ }
467
+
468
+ // Deep compare two objects and return differences
469
+ #deepCompare(obj1, obj2, path = '') {
470
+ const differences = []
471
+
472
+ // Check if both are objects
473
+ if (typeof obj1 !== typeof obj2) {
474
+ differences.push(`${path}: type mismatch (${typeof obj1} vs ${typeof obj2})`)
475
+ return {equal: false, differences}
476
+ }
477
+
478
+ // Handle null values
479
+ if (obj1 === null || obj2 === null) {
480
+ if (obj1 !== obj2) {
481
+ differences.push(`${path}: null mismatch`)
482
+ return {equal: false, differences}
483
+ }
484
+ return {equal: true, differences}
485
+ }
486
+
487
+ // Handle arrays
488
+ if (Array.isArray(obj1) && Array.isArray(obj2)) {
489
+ if (obj1.length !== obj2.length) {
490
+ differences.push(`${path}: array length mismatch (${obj1.length} vs ${obj2.length})`)
491
+ return {equal: false, differences}
492
+ }
493
+
494
+ for (let i = 0; i < obj1.length; i++) {
495
+ const result = this.#deepCompare(obj1[i], obj2[i], `${path}[${i}]`)
496
+ if (!result.equal) {
497
+ differences.push(...result.differences)
498
+ }
499
+ }
500
+
501
+ return {equal: differences.length === 0, differences}
502
+ }
503
+
504
+ // Handle objects
505
+ if (typeof obj1 === 'object' && typeof obj2 === 'object') {
506
+ const keys1 = Object.keys(obj1)
507
+ const keys2 = Object.keys(obj2)
508
+
509
+ // Check for missing keys
510
+ for (const key of keys1) {
511
+ if (!(key in obj2)) {
512
+ differences.push(`${path}.${key}: missing in second object`)
513
+ }
514
+ }
515
+
516
+ for (const key of keys2) {
517
+ if (!(key in obj1)) {
518
+ differences.push(`${path}.${key}: missing in first object`)
519
+ }
520
+ }
521
+
522
+ // Compare common keys
523
+ for (const key of keys1) {
524
+ if (key in obj2) {
525
+ const newPath = path ? `${path}.${key}` : key
526
+ const result = this.#deepCompare(obj1[key], obj2[key], newPath)
527
+ if (!result.equal) {
528
+ differences.push(...result.differences)
529
+ }
530
+ }
531
+ }
532
+
533
+ return {equal: differences.length === 0, differences}
534
+ }
535
+
536
+ // Handle primitive values
537
+ if (obj1 !== obj2) {
538
+ differences.push(`${path}: value mismatch (${obj1} vs ${obj2})`)
539
+ return {equal: false, differences}
540
+ }
541
+
542
+ return {equal: true, differences}
543
+ }
544
+
545
+ // Rollback to single-file mode if migration fails
546
+ #rollbackMigration() {
547
+ log('[Config] Rolling back to single-file mode...')
548
+
549
+ try {
550
+ // Remove modular config directory if it exists
551
+ if (fs.existsSync(this.#configDir)) {
552
+ try {
553
+ fs.rmSync(this.#configDir, {recursive: true, force: true})
554
+ log('[Config] Removed modular config directory')
555
+ } catch (err) {
556
+ error(`[Config] Failed to remove config directory: ${err.message}`)
557
+ }
558
+ }
559
+
560
+ // Restore from pre-modular backup if it exists
561
+ const preModularBackup = this.#file + '.pre-modular'
562
+ if (fs.existsSync(preModularBackup)) {
563
+ fs.copyFileSync(preModularBackup, this.#file)
564
+ log('[Config] Restored original config.json from pre-modular backup')
565
+ }
566
+
567
+ this.#isModular = false
568
+ log('[Config] Rollback completed - system is in single-file mode')
569
+ } catch (err) {
570
+ error(`[Config] Rollback failed: ${err.message}`)
571
+ error(`[Config] Error code: ${err.code}`)
572
+ error('[Config] Manual intervention may be required')
573
+ error(`[Config] Config directory: ${this.#configDir}`)
574
+ error(`[Config] Config file: ${this.#file}`)
575
+ }
576
+ }
577
+
578
+ // Fallback to single-file mode when modular operations fail
579
+ #fallbackToSingleFile() {
580
+ log('[Config] Initiating fallback to single-file mode...')
581
+
582
+ try {
583
+ // Switch to single-file mode
584
+ this.#isModular = false
585
+
586
+ // Attempt to save current config to single file
587
+ try {
588
+ this.#saveSingleFile()
589
+ log('[Config] Successfully saved config to single file')
590
+ log('[Config] System is now operating in single-file mode')
591
+ } catch (saveErr) {
592
+ error(`[Config] Failed to save to single file: ${saveErr.message}`)
593
+ error(`[Config] Error code: ${saveErr.code}`)
594
+
595
+ // Provide specific guidance based on error type
596
+ if (saveErr.code === 'EACCES' || saveErr.code === 'EPERM') {
597
+ error('[Config] Permission denied - check file system permissions')
598
+ error(`[Config] File path: ${this.#file}`)
599
+ } else if (saveErr.code === 'ENOSPC') {
600
+ error('[Config] No space left on device')
601
+ } else if (saveErr.code === 'EROFS') {
602
+ error('[Config] File system is read-only')
603
+ }
604
+
605
+ error('[Config] WARNING: Unable to save configuration')
606
+ error('[Config] Configuration changes may be lost')
607
+ }
608
+ } catch (err) {
609
+ error(`[Config] Fallback to single-file mode failed: ${err.message}`)
610
+ error('[Config] System may be in an inconsistent state')
611
+ error('[Config] Please check file system permissions and disk space')
612
+ }
613
+ }
614
+
615
+ // Save modular configuration - only writes changed modules
616
+ #saveModular() {
617
+ if (!this.#configDir) {
618
+ error('[Config] Error: Config directory not initialized')
619
+ error('[Config] Falling back to single-file mode')
620
+ this.#fallbackToSingleFile()
621
+ return
622
+ }
623
+
624
+ // Ensure config directory exists
625
+ if (!fs.existsSync(this.#configDir)) {
626
+ try {
627
+ fs.mkdirSync(this.#configDir, {recursive: true})
628
+ log(`[Config] Created config directory: ${this.#configDir}`)
629
+ } catch (err) {
630
+ error(`[Config] Failed to create config directory: ${err.message}`)
631
+ error(`[Config] Error code: ${err.code}`)
632
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
633
+ error('[Config] Permission denied - check file system permissions')
634
+ }
635
+ error('[Config] Falling back to single-file mode')
636
+ this.#fallbackToSingleFile()
637
+ return
638
+ }
639
+ }
640
+
641
+ let failedModules = []
642
+ let successfulSaves = 0
643
+
644
+ // Iterate through module mapping and save changed modules
645
+ for (const [moduleName, keys] of Object.entries(this.#moduleMap)) {
646
+ // Only write modules that have changed
647
+ if (!this.#moduleChanged[moduleName]) {
648
+ continue
649
+ }
650
+
651
+ const moduleFile = path.join(this.#configDir, moduleName + '.json')
652
+ const moduleData = {}
653
+
654
+ // Extract relevant config keys for this module
655
+ for (const key of keys) {
656
+ if (this.config[key] !== undefined) {
657
+ moduleData[key] = this.config[key]
658
+ }
659
+ }
660
+
661
+ // Use atomic write for each module file
662
+ try {
663
+ this.#atomicWrite(moduleFile, moduleData)
664
+ // Clear the changed flag after successful write
665
+ this.#moduleChanged[moduleName] = false
666
+ successfulSaves++
667
+ } catch (err) {
668
+ // Handle individual module save failures without stopping other saves
669
+ error(`[Config] Failed to save module ${moduleName}: ${err.message}`)
670
+ error(`[Config] Error code: ${err.code}`)
671
+
672
+ // Provide specific error guidance
673
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
674
+ error(`[Config] Permission denied for ${moduleFile}`)
675
+ error('[Config] Check file system permissions for the config directory')
676
+ } else if (err.code === 'ENOSPC') {
677
+ error('[Config] No space left on device')
678
+ } else if (err.code === 'EROFS') {
679
+ error('[Config] File system is read-only')
680
+ }
681
+
682
+ failedModules.push(moduleName)
683
+ // Don't clear the changed flag so we can retry on next save
684
+ }
685
+ }
686
+
687
+ // If all module saves failed and we had changes to save, fall back to single-file
688
+ if (failedModules.length > 0 && successfulSaves === 0) {
689
+ error(`[Config] All modular saves failed (${failedModules.length} modules)`)
690
+ error('[Config] Falling back to single-file mode to prevent data loss')
691
+ this.#fallbackToSingleFile()
692
+ } else if (failedModules.length > 0) {
693
+ log(`[Config] Partial save failure: ${failedModules.length} module(s) failed, ${successfulSaves} succeeded`)
694
+ log(`[Config] Failed modules: ${failedModules.join(', ')}`)
695
+ }
696
+ }
697
+
698
+ init() {
699
+ try {
700
+ this.#dir = path.join(os.homedir(), '.candypack')
701
+ this.#file = path.join(this.#dir, 'config.json')
702
+ this.#configDir = path.join(this.#dir, 'config')
703
+
704
+ // Ensure base directory exists
705
+ if (!fs.existsSync(this.#dir)) {
706
+ try {
707
+ fs.mkdirSync(this.#dir)
708
+ log(`[Config] Created base directory: ${this.#dir}`)
709
+ } catch (mkdirErr) {
710
+ error(`[Config] Failed to create base directory: ${mkdirErr.message}`)
711
+ error(`[Config] Error code: ${mkdirErr.code}`)
712
+ if (mkdirErr.code === 'EACCES' || mkdirErr.code === 'EPERM') {
713
+ error('[Config] Permission denied - check file system permissions')
714
+ }
715
+ throw mkdirErr
716
+ }
717
+ }
718
+
719
+ // Detect configuration format and load accordingly
720
+ const format = this.#detectConfigFormat()
721
+ log(`[Config] Detected configuration format: ${format}`)
722
+
723
+ switch (format) {
724
+ case 'modular':
725
+ // Load from modular config directory
726
+ log('[Config] Loading modular configuration...')
727
+ this.#isModular = true
728
+ this.#loadModular()
729
+ break
730
+
731
+ case 'single': {
732
+ // Load single-file config and migrate to modular
733
+ log('[Config] Detected single-file configuration, loading and migrating...')
734
+ try {
735
+ this.#load()
736
+ } catch (loadErr) {
737
+ error(`[Config] Failed to load single-file config: ${loadErr.message}`)
738
+ error('[Config] Initializing with default configuration')
739
+ this.config = {
740
+ server: {
741
+ pid: null,
742
+ started: null,
743
+ watchdog: null
744
+ }
745
+ }
746
+ this.#loaded = true
747
+ }
748
+
749
+ // Attempt migration to modular format
750
+ const migrationSuccess = this.#migrate()
751
+
752
+ if (migrationSuccess) {
753
+ this.#isModular = true
754
+ log('[Config] Successfully migrated to modular configuration')
755
+ } else {
756
+ // Migration failed - rollback and stay in single-file mode
757
+ error('[Config] Migration failed, rolling back to single-file mode')
758
+ this.#rollbackMigration()
759
+ this.#isModular = false
760
+ }
761
+ break
762
+ }
763
+
764
+ case 'new':
765
+ // Fresh installation - create modular structure from start
766
+ log('[Config] New installation detected, creating modular configuration structure...')
767
+ this.#isModular = true
768
+
769
+ // Create config directory
770
+ if (!fs.existsSync(this.#configDir)) {
771
+ try {
772
+ fs.mkdirSync(this.#configDir, {recursive: true})
773
+ log(`[Config] Created config directory: ${this.#configDir}`)
774
+ } catch (mkdirErr) {
775
+ error(`[Config] Failed to create config directory: ${mkdirErr.message}`)
776
+ error(`[Config] Error code: ${mkdirErr.code}`)
777
+ if (mkdirErr.code === 'EACCES' || mkdirErr.code === 'EPERM') {
778
+ error('[Config] Permission denied - check file system permissions')
779
+ }
780
+ log('[Config] Falling back to single-file mode')
781
+ this.#isModular = false
782
+ }
783
+ }
784
+
785
+ // Initialize with default config structure
786
+ this.config = {
787
+ server: {
788
+ pid: null,
789
+ started: null,
790
+ watchdog: null
791
+ }
792
+ }
793
+ this.#loaded = true
794
+ break
795
+ }
796
+
797
+ // Ensure config structure exists after loading (server object, etc.)
798
+ // This guarantees the system never enters a broken state
799
+ if (!this.config || typeof this.config !== 'object') {
800
+ log('[Config] Config object invalid, initializing with defaults')
801
+ this.config = {}
802
+ }
803
+ if (!this.config.server || typeof this.config.server !== 'object') {
804
+ log('[Config] Server config missing, initializing with defaults')
805
+ this.config.server = {
806
+ pid: null,
807
+ started: null,
808
+ watchdog: null
809
+ }
810
+ }
811
+
812
+ // Set up auto-save interval with modular support
813
+ // Handle process.mainModule safely
814
+ if (process.mainModule && process.mainModule.path && !process.mainModule.path.includes('node_modules/candypack/bin')) {
815
+ setInterval(() => this.#save(), 500).unref()
816
+ this.config = this.#proxy(this.config)
817
+ }
818
+
819
+ // Update OS and arch information
820
+ if (
821
+ !this.config.server.os ||
822
+ this.config.server.os != os.platform() ||
823
+ !this.config.server.arch ||
824
+ this.config.server.arch != os.arch()
825
+ ) {
826
+ this.config.server.os = os.platform()
827
+ this.config.server.arch = os.arch()
828
+ }
829
+
830
+ log('[Config] Initialization completed successfully')
831
+ } catch (err) {
832
+ error(`[Config] Critical initialization error: ${err.message}`)
833
+ error(`[Config] Error code: ${err.code}`)
834
+ error('[Config] Stack trace:', err.stack)
835
+
836
+ // Ensure we have a valid config object even in worst case
837
+ if (!this.config || typeof this.config !== 'object') {
838
+ error('[Config] Creating emergency default configuration')
839
+ this.config = {
840
+ server: {
841
+ pid: null,
842
+ started: null,
843
+ watchdog: null,
844
+ os: os.platform(),
845
+ arch: os.arch()
846
+ }
847
+ }
848
+ }
849
+
850
+ this.#loaded = true
851
+ this.#isModular = false
852
+ error('[Config] System initialized with minimal configuration')
853
+ error('[Config] Please check file system permissions and disk space')
854
+ }
855
+ }
856
+
857
+ #load() {
858
+ if (this.#saving && this.#loaded) return
859
+ if (!fs.existsSync(this.#file)) {
860
+ this.#loaded = true
861
+ return
862
+ }
863
+ let data = fs.readFileSync(this.#file, 'utf8')
864
+ if (!data) {
865
+ log('Error reading config file:', this.#file)
866
+ this.#loaded = true
867
+ this.#save()
868
+ return
869
+ }
870
+ try {
871
+ if (data.length > 2) {
872
+ data = JSON.parse(data)
873
+ this.#loaded = true
874
+ }
875
+ } catch {
876
+ log('Error parsing config file:', this.#file)
877
+ }
878
+ if (!this.#loaded) {
879
+ if (data.length > 2) {
880
+ const backup = path.join(this.#dir, 'config-corrupted.json')
881
+ if (fs.existsSync(this.#file)) fs.copyFileSync(this.#file, backup)
882
+ }
883
+ const bakDir = path.join(this.#dir, '.bak')
884
+ const backupFile = path.join(bakDir, 'config.json.bak')
885
+ // Try new backup location first, then fall back to old location
886
+ const backupPath = fs.existsSync(backupFile) ? backupFile : this.#file + '.bak'
887
+ if (fs.existsSync(backupPath)) {
888
+ data = fs.readFileSync(backupPath, 'utf8')
889
+ if (!data) {
890
+ error('Error reading backup file:', backupPath)
891
+ this.#save(true)
892
+ return
893
+ }
894
+ try {
895
+ data = JSON.parse(data)
896
+ fs.promises.writeFile(this.#file, JSON.stringify(data, null, 4), 'utf8')
897
+ } catch (e) {
898
+ error(e)
899
+ this.#save(true)
900
+ return
901
+ }
902
+ if (data && typeof data === 'object') {
903
+ this.config = data
904
+ }
905
+ return
906
+ } else {
907
+ this.config = {}
908
+ this.#save(true)
909
+ return
910
+ }
911
+ } else {
912
+ if (data && typeof data === 'object') {
913
+ this.config = data
914
+ }
915
+ return
916
+ }
917
+ }
918
+
919
+ #proxy(target, parentKey = null) {
920
+ if (typeof target !== 'object' || target === null) return target
921
+
922
+ const handler = {
923
+ get: (obj, prop) => {
924
+ const value = obj[prop]
925
+ if (typeof value === 'object' && value !== null) {
926
+ // Pass the top-level key down for tracking nested changes
927
+ const topKey = parentKey || prop
928
+ return this.#proxy(value, topKey)
929
+ }
930
+ return value
931
+ },
932
+ set: (obj, prop, value) => {
933
+ // Mark config as changed
934
+ this.#changed = true
935
+
936
+ // Track which module this change belongs to
937
+ // Use parentKey if we're in a nested object, otherwise use the current prop
938
+ const topKey = parentKey || prop
939
+ const moduleName = this.#getModuleForKey(topKey)
940
+ if (moduleName) {
941
+ this.#moduleChanged[moduleName] = true
942
+ }
943
+
944
+ // Set the value, wrapping objects/arrays in proxy for nested tracking
945
+ if (typeof value === 'object' && value !== null) {
946
+ obj[prop] = this.#proxy(value, topKey)
947
+ } else {
948
+ obj[prop] = value
949
+ }
950
+
951
+ return true
952
+ },
953
+ deleteProperty: (obj, prop) => {
954
+ // Mark config as changed
955
+ this.#changed = true
956
+
957
+ // Track which module this change belongs to
958
+ const topKey = parentKey || prop
959
+ const moduleName = this.#getModuleForKey(topKey)
960
+ if (moduleName) {
961
+ this.#moduleChanged[moduleName] = true
962
+ }
963
+
964
+ delete obj[prop]
965
+ return true
966
+ }
967
+ }
968
+
969
+ return new Proxy(target, handler)
970
+ }
971
+
972
+ reload() {
973
+ log('[Config] Reloading configuration...')
974
+
975
+ try {
976
+ this.#loaded = false
977
+
978
+ // Detect current format before reloading
979
+ const format = this.#detectConfigFormat()
980
+
981
+ if (format === 'modular' || this.#isModular) {
982
+ log('[Config] Reloading from modular format')
983
+ this.#loadModular()
984
+ } else {
985
+ log('[Config] Reloading from single-file format')
986
+ this.#load()
987
+ }
988
+
989
+ // Reset module change tracking flags after reload
990
+ this.#moduleChanged = {}
991
+
992
+ log('[Config] Configuration reloaded successfully')
993
+ } catch (err) {
994
+ error(`[Config] Failed to reload configuration: ${err.message}`)
995
+ error(`[Config] Error code: ${err.code}`)
996
+ error('[Config] Keeping existing configuration in memory')
997
+
998
+ // Ensure we still have a valid config
999
+ if (!this.config || typeof this.config !== 'object') {
1000
+ error('[Config] Configuration corrupted, initializing with defaults')
1001
+ this.config = {
1002
+ server: {
1003
+ pid: null,
1004
+ started: null,
1005
+ watchdog: null,
1006
+ os: os.platform(),
1007
+ arch: os.arch()
1008
+ }
1009
+ }
1010
+ }
1011
+
1012
+ this.#loaded = true
1013
+ }
1014
+ }
1015
+
1016
+ #save() {
1017
+ // Maintain existing save debouncing and change detection
1018
+ if (this.#saving || !this.#changed) return
1019
+ this.#changed = false
1020
+ this.#saving = true
1021
+
1022
+ try {
1023
+ // Check if system is in modular mode
1024
+ if (this.#isModular) {
1025
+ // Call modular save method if in modular mode
1026
+ this.#saveModular()
1027
+ } else {
1028
+ // Fall back to single-file save for backward compatibility
1029
+ this.#saveSingleFile()
1030
+ }
1031
+ } catch (err) {
1032
+ error(`[Config] Save operation failed: ${err.message}`)
1033
+ error(`[Config] Error code: ${err.code}`)
1034
+
1035
+ // Provide specific error guidance
1036
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
1037
+ error('[Config] Permission denied - check file system permissions')
1038
+ } else if (err.code === 'ENOSPC') {
1039
+ error('[Config] No space left on device')
1040
+ } else if (err.code === 'EROFS') {
1041
+ error('[Config] File system is read-only')
1042
+ }
1043
+
1044
+ // Mark as changed again so we can retry on next interval
1045
+ this.#changed = true
1046
+ log('[Config] Configuration changes will be retried on next save interval')
1047
+ } finally {
1048
+ this.#saving = false
1049
+ }
1050
+ }
1051
+
1052
+ // Single-file save method for backward compatibility
1053
+ #saveSingleFile() {
1054
+ try {
1055
+ let json = JSON.stringify(this.config, null, 4)
1056
+ if (json.length < 3) json = '{}'
1057
+
1058
+ // Write main config file
1059
+ fs.writeFileSync(this.#file, json, 'utf8')
1060
+
1061
+ // Write backup file to .bak directory after a delay
1062
+ setTimeout(() => {
1063
+ try {
1064
+ const bakDir = path.join(this.#dir, '.bak')
1065
+ // Ensure .bak directory exists
1066
+ if (!fs.existsSync(bakDir)) {
1067
+ fs.mkdirSync(bakDir, {recursive: true})
1068
+ }
1069
+ fs.writeFileSync(path.join(bakDir, 'config.json.bak'), json, 'utf8')
1070
+ } catch (backupErr) {
1071
+ error(`[Config] Failed to write backup file: ${backupErr.message}`)
1072
+ error(`[Config] Error code: ${backupErr.code}`)
1073
+ // Don't throw - backup failure shouldn't stop the main save
1074
+ }
1075
+ }, 5000)
1076
+ } catch (err) {
1077
+ error(`[Config] Failed to save single-file config: ${err.message}`)
1078
+ error(`[Config] Error code: ${err.code}`)
1079
+
1080
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
1081
+ error(`[Config] Permission denied for ${this.#file}`)
1082
+ error('[Config] Check file system permissions')
1083
+ } else if (err.code === 'ENOSPC') {
1084
+ error('[Config] No space left on device')
1085
+ } else if (err.code === 'EROFS') {
1086
+ error('[Config] File system is read-only')
1087
+ }
1088
+
1089
+ throw err
1090
+ }
1091
+ }
1092
+ }
1093
+
1094
+ module.exports = Config