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.
- package/.editorconfig +21 -0
- package/.github/workflows/auto-pr-description.yml +49 -0
- package/.github/workflows/release.yml +32 -0
- package/.github/workflows/test-coverage.yml +58 -0
- package/.husky/pre-commit +2 -0
- package/.kiro/steering/code-style.md +56 -0
- package/.kiro/steering/product.md +20 -0
- package/.kiro/steering/structure.md +77 -0
- package/.kiro/steering/tech.md +87 -0
- package/.prettierrc +10 -0
- package/.releaserc.js +134 -0
- package/AGENTS.md +84 -0
- package/CHANGELOG.md +181 -0
- package/CODE_OF_CONDUCT.md +83 -0
- package/CONTRIBUTING.md +63 -0
- package/LICENSE +661 -0
- package/README.md +57 -0
- package/SECURITY.md +26 -0
- package/bin/candy +10 -0
- package/bin/candypack +10 -0
- package/cli/index.js +3 -0
- package/cli/src/Cli.js +348 -0
- package/cli/src/Connector.js +93 -0
- package/cli/src/Monitor.js +416 -0
- package/core/Candy.js +87 -0
- package/core/Commands.js +239 -0
- package/core/Config.js +1094 -0
- package/core/Lang.js +52 -0
- package/core/Log.js +43 -0
- package/core/Process.js +26 -0
- package/docs/backend/01-overview/01-whats-in-the-candy-box.md +9 -0
- package/docs/backend/01-overview/02-super-handy-helper-functions.md +9 -0
- package/docs/backend/01-overview/03-development-server.md +79 -0
- package/docs/backend/02-structure/01-typical-project-layout.md +39 -0
- package/docs/backend/03-config/00-configuration-overview.md +214 -0
- package/docs/backend/03-config/01-database-connection.md +60 -0
- package/docs/backend/03-config/02-static-route-mapping-optional.md +20 -0
- package/docs/backend/03-config/03-request-timeout.md +11 -0
- package/docs/backend/03-config/04-environment-variables.md +227 -0
- package/docs/backend/03-config/05-early-hints.md +352 -0
- package/docs/backend/04-routing/01-basic-page-routes.md +28 -0
- package/docs/backend/04-routing/02-controller-less-view-routes.md +43 -0
- package/docs/backend/04-routing/03-api-and-data-routes.md +20 -0
- package/docs/backend/04-routing/04-authentication-aware-routes.md +48 -0
- package/docs/backend/04-routing/05-advanced-routing.md +14 -0
- package/docs/backend/04-routing/06-error-pages.md +101 -0
- package/docs/backend/04-routing/07-cron-jobs.md +149 -0
- package/docs/backend/05-controllers/01-how-to-build-a-controller.md +17 -0
- package/docs/backend/05-controllers/02-your-trusty-candy-assistant.md +20 -0
- package/docs/backend/05-controllers/03-controller-classes.md +93 -0
- package/docs/backend/05-forms/01-custom-forms.md +395 -0
- package/docs/backend/05-forms/02-automatic-database-insert.md +297 -0
- package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +96 -0
- package/docs/backend/06-request-and-response/02-sending-a-response-replying-to-the-user.md +40 -0
- package/docs/backend/07-views/01-the-view-directory.md +73 -0
- package/docs/backend/07-views/02-rendering-a-view.md +179 -0
- package/docs/backend/07-views/03-template-syntax.md +181 -0
- package/docs/backend/07-views/03-variables.md +328 -0
- package/docs/backend/07-views/04-request-data.md +231 -0
- package/docs/backend/07-views/05-conditionals.md +290 -0
- package/docs/backend/07-views/06-loops.md +353 -0
- package/docs/backend/07-views/07-translations.md +358 -0
- package/docs/backend/07-views/08-backend-javascript.md +398 -0
- package/docs/backend/07-views/09-comments.md +297 -0
- package/docs/backend/08-database/01-database-connection.md +99 -0
- package/docs/backend/08-database/02-using-mysql.md +322 -0
- package/docs/backend/09-validation/01-the-validator-service.md +424 -0
- package/docs/backend/10-authentication/01-user-logins-with-authjs.md +53 -0
- package/docs/backend/10-authentication/02-foiling-villains-with-csrf-protection.md +55 -0
- package/docs/backend/10-authentication/03-register.md +134 -0
- package/docs/backend/10-authentication/04-candy-register-forms.md +676 -0
- package/docs/backend/10-authentication/05-session-management.md +159 -0
- package/docs/backend/10-authentication/06-candy-login-forms.md +596 -0
- package/docs/backend/11-mail/01-the-mail-service.md +42 -0
- package/docs/backend/12-streaming/01-streaming-overview.md +300 -0
- package/docs/backend/13-utilities/01-candy-var.md +504 -0
- package/docs/frontend/01-overview/01-introduction.md +146 -0
- package/docs/frontend/02-ajax-navigation/01-quick-start.md +608 -0
- package/docs/frontend/02-ajax-navigation/02-configuration.md +370 -0
- package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +519 -0
- package/docs/frontend/03-forms/01-form-handling.md +420 -0
- package/docs/frontend/04-api-requests/01-get-post.md +443 -0
- package/docs/frontend/05-streaming/01-client-streaming.md +163 -0
- package/docs/index.json +452 -0
- package/docs/server/01-installation/01-quick-install.md +19 -0
- package/docs/server/01-installation/02-manual-installation-via-npm.md +9 -0
- package/docs/server/02-get-started/01-core-concepts.md +7 -0
- package/docs/server/02-get-started/02-basic-commands.md +57 -0
- package/docs/server/02-get-started/03-cli-reference.md +276 -0
- package/docs/server/02-get-started/04-cli-quick-reference.md +102 -0
- package/docs/server/03-service/01-start-a-new-service.md +57 -0
- package/docs/server/03-service/02-delete-a-service.md +48 -0
- package/docs/server/04-web/01-create-a-website.md +36 -0
- package/docs/server/04-web/02-list-websites.md +9 -0
- package/docs/server/04-web/03-delete-a-website.md +29 -0
- package/docs/server/05-subdomain/01-create-a-subdomain.md +32 -0
- package/docs/server/05-subdomain/02-list-subdomains.md +33 -0
- package/docs/server/05-subdomain/03-delete-a-subdomain.md +41 -0
- package/docs/server/06-ssl/01-renew-an-ssl-certificate.md +34 -0
- package/docs/server/07-mail/01-create-a-mail-account.md +23 -0
- package/docs/server/07-mail/02-delete-a-mail-account.md +20 -0
- package/docs/server/07-mail/03-list-mail-accounts.md +20 -0
- package/docs/server/07-mail/04-change-account-password.md +23 -0
- package/eslint.config.mjs +120 -0
- package/framework/index.js +4 -0
- package/framework/src/Auth.js +309 -0
- package/framework/src/Candy.js +81 -0
- package/framework/src/Config.js +79 -0
- package/framework/src/Env.js +60 -0
- package/framework/src/Lang.js +57 -0
- package/framework/src/Mail.js +83 -0
- package/framework/src/Mysql.js +575 -0
- package/framework/src/Request.js +301 -0
- package/framework/src/Route/Cron.js +128 -0
- package/framework/src/Route/Internal.js +439 -0
- package/framework/src/Route.js +455 -0
- package/framework/src/Server.js +15 -0
- package/framework/src/Stream.js +163 -0
- package/framework/src/Token.js +37 -0
- package/framework/src/Validator.js +271 -0
- package/framework/src/Var.js +211 -0
- package/framework/src/View/EarlyHints.js +190 -0
- package/framework/src/View/Form.js +600 -0
- package/framework/src/View.js +513 -0
- package/framework/web/candy.js +838 -0
- package/jest.config.js +22 -0
- package/locale/de-DE.json +80 -0
- package/locale/en-US.json +79 -0
- package/locale/es-ES.json +80 -0
- package/locale/fr-FR.json +80 -0
- package/locale/pt-BR.json +80 -0
- package/locale/ru-RU.json +80 -0
- package/locale/tr-TR.json +85 -0
- package/locale/zh-CN.json +80 -0
- package/package.json +86 -0
- package/server/index.js +5 -0
- package/server/src/Api.js +88 -0
- package/server/src/DNS.js +940 -0
- package/server/src/Hub.js +535 -0
- package/server/src/Mail.js +571 -0
- package/server/src/SSL.js +180 -0
- package/server/src/Server.js +27 -0
- package/server/src/Service.js +248 -0
- package/server/src/Subdomain.js +64 -0
- package/server/src/Web/Firewall.js +170 -0
- package/server/src/Web/Proxy.js +134 -0
- package/server/src/Web.js +451 -0
- package/server/src/mail/imap.js +1091 -0
- package/server/src/mail/server.js +32 -0
- package/server/src/mail/smtp.js +786 -0
- package/test/cli/Cli.test.js +36 -0
- package/test/core/Candy.test.js +234 -0
- package/test/core/Commands.test.js +538 -0
- package/test/core/Config.test.js +1435 -0
- package/test/core/Lang.test.js +250 -0
- package/test/core/Process.test.js +156 -0
- package/test/framework/Route.test.js +239 -0
- package/test/framework/View/EarlyHints.test.js +282 -0
- package/test/scripts/check-coverage.js +132 -0
- package/test/server/Api.test.js +647 -0
- package/test/server/Client.test.js +338 -0
- package/test/server/DNS.test.js +2050 -0
- package/test/server/DNS.test.js.bak +2084 -0
- package/test/server/Log.test.js +73 -0
- package/test/server/Mail.account.test_.js +460 -0
- package/test/server/Mail.init.test_.js +411 -0
- package/test/server/Mail.test_.js +1340 -0
- package/test/server/SSL.test_.js +1491 -0
- package/test/server/Server.test.js +765 -0
- package/test/server/Service.test_.js +1127 -0
- package/test/server/Subdomain.test.js +440 -0
- package/test/server/Web/Firewall.test.js +175 -0
- package/test/server/Web.test_.js +1562 -0
- package/test/server/__mocks__/acme-client.js +17 -0
- package/test/server/__mocks__/bcrypt.js +50 -0
- package/test/server/__mocks__/child_process.js +389 -0
- package/test/server/__mocks__/crypto.js +432 -0
- package/test/server/__mocks__/fs.js +450 -0
- package/test/server/__mocks__/globalCandy.js +227 -0
- package/test/server/__mocks__/http-proxy.js +105 -0
- package/test/server/__mocks__/http.js +575 -0
- package/test/server/__mocks__/https.js +272 -0
- package/test/server/__mocks__/index.js +249 -0
- package/test/server/__mocks__/mail/server.js +100 -0
- package/test/server/__mocks__/mail/smtp.js +31 -0
- package/test/server/__mocks__/mailparser.js +81 -0
- package/test/server/__mocks__/net.js +369 -0
- package/test/server/__mocks__/node-forge.js +328 -0
- package/test/server/__mocks__/os.js +320 -0
- package/test/server/__mocks__/path.js +291 -0
- package/test/server/__mocks__/selfsigned.js +8 -0
- package/test/server/__mocks__/server/src/mail/server.js +100 -0
- package/test/server/__mocks__/server/src/mail/smtp.js +31 -0
- package/test/server/__mocks__/smtp-server.js +106 -0
- package/test/server/__mocks__/sqlite3.js +394 -0
- package/test/server/__mocks__/testFactories.js +299 -0
- package/test/server/__mocks__/testHelpers.js +363 -0
- package/test/server/__mocks__/tls.js +229 -0
- package/watchdog/index.js +3 -0
- package/watchdog/src/Watchdog.js +156 -0
- package/web/config.json +5 -0
- package/web/controller/page/about.js +27 -0
- package/web/controller/page/index.js +34 -0
- package/web/package.json +18 -0
- package/web/public/assets/css/style.css +1835 -0
- package/web/public/assets/js/app.js +96 -0
- package/web/route/www.js +19 -0
- package/web/skeleton/main.html +22 -0
- package/web/view/content/about.html +65 -0
- package/web/view/content/home.html +205 -0
- package/web/view/footer/main.html +11 -0
- package/web/view/head/main.html +5 -0
- 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
|