claw-dashboard 1.9.0 → 2.1.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 (135) hide show
  1. package/.c8rc.json +38 -0
  2. package/.dockerignore +68 -0
  3. package/.github/dependabot.yml +45 -0
  4. package/.github/pull_request_template.md +39 -0
  5. package/.github/workflows/ci.yml +147 -0
  6. package/.github/workflows/release.yml +40 -0
  7. package/.github/workflows/security.yml +84 -0
  8. package/.husky/pre-commit +1 -0
  9. package/.planning/codebase/ARCHITECTURE.md +206 -0
  10. package/.planning/codebase/INTEGRATIONS.md +150 -0
  11. package/.planning/codebase/STACK.md +122 -0
  12. package/.planning/codebase/STRUCTURE.md +201 -0
  13. package/CHANGELOG.md +293 -0
  14. package/CONTRIBUTING.md +378 -0
  15. package/Dockerfile +54 -0
  16. package/FEATURES.md +195 -21
  17. package/README.md +83 -6
  18. package/TODO.md +28 -0
  19. package/build-cjs.js +127 -0
  20. package/cjs-shim.js +13 -0
  21. package/dist/clawdash +1729 -0
  22. package/dist/clawdash.meta.json +2236 -0
  23. package/dist/widgets.cjs +6511 -0
  24. package/docker-compose.yml +67 -0
  25. package/docs/API.md +1050 -0
  26. package/docs/PLUGINS.md +1504 -0
  27. package/esbuild.config.js +158 -0
  28. package/eslint.config.js +56 -0
  29. package/examples/plugins/README.md +122 -0
  30. package/examples/plugins/api-status/index.js +294 -0
  31. package/examples/plugins/api-status/plugin.json +19 -0
  32. package/examples/plugins/hello-world/index.js +104 -0
  33. package/examples/plugins/hello-world/plugin.json +15 -0
  34. package/examples/plugins/system-metrics-chart/index.js +339 -0
  35. package/examples/plugins/system-metrics-chart/plugin.json +18 -0
  36. package/examples/plugins/weather-widget/index.js +136 -0
  37. package/examples/plugins/weather-widget/plugin.json +19 -0
  38. package/index.cjs +23005 -0
  39. package/index.js +5285 -566
  40. package/jest.config.js +11 -0
  41. package/man/clawdash.1 +276 -0
  42. package/package.json +54 -6
  43. package/schemas/plugin-manifest.json +128 -0
  44. package/scripts/release.js +595 -0
  45. package/src/alerts.js +693 -0
  46. package/src/auto-save.js +584 -0
  47. package/src/cache.js +390 -0
  48. package/src/checksum.js +146 -0
  49. package/src/cli/args.js +110 -0
  50. package/src/cli/export-schedule.js +423 -0
  51. package/src/cli/export-snapshot.js +138 -0
  52. package/src/cli/help.js +69 -0
  53. package/src/cli/import-snapshot.js +230 -0
  54. package/src/cli/index.js +14 -0
  55. package/src/cli/list-templates.js +69 -0
  56. package/src/cli/validate-config.js +76 -0
  57. package/src/cli/validate-plugin.js +187 -0
  58. package/src/cli/version.js +15 -0
  59. package/src/config-validator.js +586 -0
  60. package/src/config-watcher.js +401 -0
  61. package/src/config.js +684 -0
  62. package/src/container-detector.js +499 -0
  63. package/src/database.js +734 -0
  64. package/src/differential-render.js +327 -0
  65. package/src/errors.js +169 -0
  66. package/src/export-scheduler.js +581 -0
  67. package/src/gateway-manager.js +685 -0
  68. package/src/hints.js +371 -0
  69. package/src/loading-states.js +509 -0
  70. package/src/logger.js +185 -0
  71. package/src/memory-pressure.js +467 -0
  72. package/src/performance-monitor.js +374 -0
  73. package/src/plugin-errors.js +586 -0
  74. package/src/plugin-manifest-validator.js +219 -0
  75. package/src/plugin-reload.js +481 -0
  76. package/src/plugin-scaffold.js +1934 -0
  77. package/src/plugin-validator.js +284 -0
  78. package/src/retry.js +218 -0
  79. package/src/security.js +860 -0
  80. package/src/snapshot.js +372 -0
  81. package/src/splash.js +115 -0
  82. package/src/theme-selector.js +411 -0
  83. package/src/themes.js +874 -0
  84. package/src/transitions.js +534 -0
  85. package/src/types.d.ts +174 -0
  86. package/src/validation.js +926 -0
  87. package/src/web-server.js +885 -0
  88. package/src/widgets/builtin-widgets.js +1056 -0
  89. package/src/widgets/config-processor.js +401 -0
  90. package/src/widgets/dependency-resolver.js +596 -0
  91. package/src/widgets/index.js +79 -0
  92. package/src/widgets/plugin-api.js +845 -0
  93. package/src/widgets/widget-error-boundary.js +773 -0
  94. package/src/widgets/widget-error-isolation.js +551 -0
  95. package/src/widgets/widget-loader.js +1437 -0
  96. package/src/workers/system-worker.js +130 -0
  97. package/src/workers/worker-pool.js +633 -0
  98. package/tests/alerts.test.js +437 -0
  99. package/tests/auto-save.test.js +529 -0
  100. package/tests/cache.test.js +317 -0
  101. package/tests/cli.test.js +351 -0
  102. package/tests/command-palette.test.js +320 -0
  103. package/tests/config-processor.test.js +452 -0
  104. package/tests/config-validator.test.js +710 -0
  105. package/tests/config-watcher.test.js +594 -0
  106. package/tests/config.test.js +755 -0
  107. package/tests/database.test.js +438 -0
  108. package/tests/errors.test.js +189 -0
  109. package/tests/example-plugins.test.js +624 -0
  110. package/tests/integration.test.js +1321 -0
  111. package/tests/loading-states.test.js +300 -0
  112. package/tests/manifest-validation-on-load.test.js +671 -0
  113. package/tests/performance-monitor.test.js +190 -0
  114. package/tests/plugin-api-rate-limit.test.js +302 -0
  115. package/tests/plugin-errors.test.js +311 -0
  116. package/tests/plugin-lifecycle-e2e.test.js +1036 -0
  117. package/tests/plugin-reload.test.js +764 -0
  118. package/tests/rate-limiter.test.js +539 -0
  119. package/tests/retry.test.js +308 -0
  120. package/tests/security.test.js +411 -0
  121. package/tests/settings-modal.test.js +226 -0
  122. package/tests/theme-selector.test.js +57 -0
  123. package/tests/utils.js +242 -0
  124. package/tests/utils.test.js +317 -0
  125. package/tests/validate-plugin-cli.test.js +359 -0
  126. package/tests/validation.test.js +837 -0
  127. package/tests/web-server.test.js +646 -0
  128. package/tests/widget-arrange-mode.test.js +183 -0
  129. package/tests/widget-config-hot-reload.test.js +465 -0
  130. package/tests/widget-dependency.test.js +591 -0
  131. package/tests/widget-error-boundary.test.js +749 -0
  132. package/tests/widget-error-isolation.test.js +469 -0
  133. package/tests/widget-integration.test.js +1147 -0
  134. package/tests/widget-refresh-intervals.test.js +284 -0
  135. package/tests/worker-pool.test.js +522 -0
@@ -0,0 +1,586 @@
1
+ /**
2
+ * Dashboard Configuration Validator
3
+ * Validates dashboard-settings.json files
4
+ */
5
+
6
+ import { existsSync, readFileSync } from 'fs';
7
+ import { resolve, dirname } from 'path';
8
+ import {
9
+ VALIDATION,
10
+ GATEWAY,
11
+ WEB,
12
+ DEFAULT_SETTINGS,
13
+ PATHS
14
+ } from './config.js';
15
+
16
+ /**
17
+ * Validation result type
18
+ * @typedef {Object} ConfigValidationResult
19
+ * @property {boolean} valid - Whether the config is valid
20
+ * @property {string[]} errors - Array of error messages
21
+ * @property {string[]} warnings - Array of warning messages
22
+ * @property {string[]} info - Array of info messages
23
+ * @property {Object} stats - Validation statistics
24
+ */
25
+
26
+ /**
27
+ * Validate a config value type
28
+ * @private
29
+ * @param {*} value - The value to check
30
+ * @param {string} expectedType - Expected type
31
+ * @param {string} path - Path in config
32
+ * @returns {string|null} Error message or null
33
+ */
34
+ function validateType(value, expectedType, path) {
35
+ if (value === undefined || value === null) {
36
+ return null; // Null/undefined handled separately
37
+ }
38
+
39
+ const actualType = Array.isArray(value) ? 'array' : typeof value;
40
+
41
+ if (expectedType === 'integer') {
42
+ if (!Number.isInteger(value)) {
43
+ return `'${path}' must be an integer, got ${actualType}`;
44
+ }
45
+ return null;
46
+ }
47
+
48
+ if (expectedType === 'port') {
49
+ if (!Number.isInteger(value) || value < 1 || value > 65535) {
50
+ return `'${path}' must be a valid port number (1-65535), got ${value}`;
51
+ }
52
+ return null;
53
+ }
54
+
55
+ if (actualType !== expectedType) {
56
+ return `'${path}' must be of type ${expectedType}, got ${actualType}`;
57
+ }
58
+
59
+ return null;
60
+ }
61
+
62
+ /**
63
+ * Validate gateway endpoint configuration
64
+ * @private
65
+ * @param {Object} endpoint - Endpoint config
66
+ * @param {number} index - Index in array
67
+ * @returns {Object} { errors: string[], warnings: string[] }
68
+ */
69
+ function validateGatewayEndpoint(endpoint, index) {
70
+ const errors = [];
71
+ const warnings = [];
72
+ const path = `gatewayEndpoints[${index}]`;
73
+
74
+ if (!endpoint || typeof endpoint !== 'object') {
75
+ errors.push(`'${path}' must be an object`);
76
+ return { errors, warnings };
77
+ }
78
+
79
+ // Required fields
80
+ if (!('name' in endpoint)) {
81
+ errors.push(`'${path}.name' is required`);
82
+ } else if (typeof endpoint.name === 'string') {
83
+ const nameLen = endpoint.name.length;
84
+ if (nameLen < VALIDATION.ENDPOINT_NAME.MIN_LENGTH) {
85
+ errors.push(`'${path}.name' must be at least ${VALIDATION.ENDPOINT_NAME.MIN_LENGTH} character(s)`);
86
+ }
87
+ if (nameLen > VALIDATION.ENDPOINT_NAME.MAX_LENGTH) {
88
+ errors.push(`'${path}.name' must be at most ${VALIDATION.ENDPOINT_NAME.MAX_LENGTH} characters`);
89
+ }
90
+ if (!VALIDATION.ENDPOINT_NAME.PATTERN.test(endpoint.name)) {
91
+ errors.push(`'${path}.name' must match pattern: ${VALIDATION.ENDPOINT_NAME.PATTERN.source}`);
92
+ }
93
+ }
94
+
95
+ if (!('host' in endpoint)) {
96
+ errors.push(`'${path}.host' is required`);
97
+ } else {
98
+ const hostError = validateType(endpoint.host, 'string', `${path}.host`);
99
+ if (hostError) errors.push(hostError);
100
+ }
101
+
102
+ if (!('port' in endpoint)) {
103
+ errors.push(`'${path}.port' is required`);
104
+ } else {
105
+ const portError = validateType(endpoint.port, 'port', `${path}.port`);
106
+ if (portError) errors.push(portError);
107
+ }
108
+
109
+ // Optional fields
110
+ if ('enabled' in endpoint) {
111
+ const enabledError = validateType(endpoint.enabled, 'boolean', `${path}.enabled`);
112
+ if (enabledError) errors.push(enabledError);
113
+ }
114
+
115
+ if ('type' in endpoint) {
116
+ if (!VALIDATION.VALID_ENDPOINT_TYPES.includes(endpoint.type)) {
117
+ errors.push(`'${path}.type' must be one of: ${VALIDATION.VALID_ENDPOINT_TYPES.join(', ')}`);
118
+ }
119
+ }
120
+
121
+ if ('token' in endpoint && endpoint.token !== null) {
122
+ const tokenError = validateType(endpoint.token, 'string', `${path}.token`);
123
+ if (tokenError) errors.push(tokenError);
124
+ }
125
+
126
+ // Warn about extra fields
127
+ const knownFields = ['name', 'host', 'port', 'enabled', 'type', 'token'];
128
+ const extraFields = Object.keys(endpoint).filter(k => !knownFields.includes(k));
129
+ for (const field of extraFields) {
130
+ warnings.push(`'${path}.${field}' is not a standard endpoint field`);
131
+ }
132
+
133
+ return { errors, warnings };
134
+ }
135
+
136
+ /**
137
+ * Validate web interface configuration
138
+ * @private
139
+ * @param {Object} webConfig - Web interface config
140
+ * @returns {Object} { errors: string[], warnings: string[] }
141
+ */
142
+ function validateWebInterfaceConfig(webConfig) {
143
+ const errors = [];
144
+ const warnings = [];
145
+ const path = 'webInterface';
146
+
147
+ if (!webConfig || typeof webConfig !== 'object') {
148
+ errors.push(`'${path}' must be an object`);
149
+ return { errors, warnings };
150
+ }
151
+
152
+ // enabled
153
+ if ('enabled' in webConfig) {
154
+ const err = validateType(webConfig.enabled, 'boolean', `${path}.enabled`);
155
+ if (err) errors.push(err);
156
+ }
157
+
158
+ // port
159
+ if ('port' in webConfig) {
160
+ const err = validateType(webConfig.port, 'port', `${path}.port`);
161
+ if (err) errors.push(err);
162
+ }
163
+
164
+ // host
165
+ if ('host' in webConfig) {
166
+ const err = validateType(webConfig.host, 'string', `${path}.host`);
167
+ if (err) errors.push(err);
168
+ }
169
+
170
+ // cors
171
+ if ('cors' in webConfig) {
172
+ const err = validateType(webConfig.cors, 'boolean', `${path}.cors`);
173
+ if (err) errors.push(err);
174
+ }
175
+
176
+ // corsOrigins - can be string or array
177
+ if ('corsOrigins' in webConfig) {
178
+ const origins = webConfig.corsOrigins;
179
+ if (typeof origins !== 'string' && !Array.isArray(origins)) {
180
+ errors.push(`'${path}.corsOrigins' must be a string or array`);
181
+ } else if (Array.isArray(origins)) {
182
+ for (let i = 0; i < origins.length; i++) {
183
+ if (typeof origins[i] !== 'string') {
184
+ errors.push(`'${path}.corsOrigins[${i}]' must be a string`);
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ // rateLimit
191
+ if ('rateLimit' in webConfig) {
192
+ const rl = webConfig.rateLimit;
193
+ if (!rl || typeof rl !== 'object') {
194
+ errors.push(`'${path}.rateLimit' must be an object`);
195
+ } else {
196
+ if ('enabled' in rl) {
197
+ const err = validateType(rl.enabled, 'boolean', `${path}.rateLimit.enabled`);
198
+ if (err) errors.push(err);
199
+ }
200
+ if ('windowMs' in rl) {
201
+ const err = validateType(rl.windowMs, 'integer', `${path}.rateLimit.windowMs`);
202
+ if (err) errors.push(err);
203
+ else if (rl.windowMs < 1000) {
204
+ warnings.push(`'${path}.rateLimit.windowMs' is less than 1 second (${rl.windowMs}ms)`);
205
+ }
206
+ }
207
+ if ('maxRequests' in rl) {
208
+ const err = validateType(rl.maxRequests, 'integer', `${path}.rateLimit.maxRequests`);
209
+ if (err) errors.push(err);
210
+ else if (rl.maxRequests < 1) {
211
+ errors.push(`'${path}.rateLimit.maxRequests' must be at least 1`);
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ // auth
218
+ if ('auth' in webConfig) {
219
+ const auth = webConfig.auth;
220
+ if (!auth || typeof auth !== 'object') {
221
+ errors.push(`'${path}.auth' must be an object`);
222
+ } else {
223
+ if ('enabled' in auth) {
224
+ const err = validateType(auth.enabled, 'boolean', `${path}.auth.enabled`);
225
+ if (err) errors.push(err);
226
+ }
227
+ if ('keys' in auth) {
228
+ if (!Array.isArray(auth.keys)) {
229
+ errors.push(`'${path}.auth.keys' must be an array`);
230
+ } else {
231
+ for (let i = 0; i < auth.keys.length; i++) {
232
+ const key = auth.keys[i];
233
+ if (!key || typeof key !== 'object') {
234
+ errors.push(`'${path}.auth.keys[${i}]' must be an object`);
235
+ }
236
+ }
237
+ }
238
+ }
239
+ }
240
+ }
241
+
242
+ return { errors, warnings };
243
+ }
244
+
245
+ /**
246
+ * Validate widget loading configuration
247
+ * @private
248
+ * @param {Object} widgetConfig - Widget loading config
249
+ * @returns {Object} { errors: string[], warnings: string[] }
250
+ */
251
+ function validateWidgetLoadingConfig(widgetConfig) {
252
+ const errors = [];
253
+ const warnings = [];
254
+ const path = 'widgetLoading';
255
+
256
+ if (!widgetConfig || typeof widgetConfig !== 'object') {
257
+ errors.push(`'${path}' must be an object`);
258
+ return { errors, warnings };
259
+ }
260
+
261
+ // enabled
262
+ if ('enabled' in widgetConfig) {
263
+ const err = validateType(widgetConfig.enabled, 'boolean', `${path}.enabled`);
264
+ if (err) errors.push(err);
265
+ }
266
+
267
+ // preloadPriority
268
+ if ('preloadPriority' in widgetConfig) {
269
+ if (!Array.isArray(widgetConfig.preloadPriority)) {
270
+ errors.push(`'${path}.preloadPriority' must be an array`);
271
+ } else {
272
+ for (let i = 0; i < widgetConfig.preloadPriority.length; i++) {
273
+ if (typeof widgetConfig.preloadPriority[i] !== 'string') {
274
+ errors.push(`'${path}.preloadPriority[${i}]' must be a string`);
275
+ }
276
+ }
277
+ }
278
+ }
279
+
280
+ // lazyLoadDelay
281
+ if ('lazyLoadDelay' in widgetConfig) {
282
+ const err = validateType(widgetConfig.lazyLoadDelay, 'integer', `${path}.lazyLoadDelay`);
283
+ if (err) errors.push(err);
284
+ else if (widgetConfig.lazyLoadDelay < 0) {
285
+ errors.push(`'${path}.lazyLoadDelay' must be non-negative`);
286
+ }
287
+ }
288
+
289
+ // maxConcurrent
290
+ if ('maxConcurrent' in widgetConfig) {
291
+ const err = validateType(widgetConfig.maxConcurrent, 'integer', `${path}.maxConcurrent`);
292
+ if (err) errors.push(err);
293
+ else if (widgetConfig.maxConcurrent < 1) {
294
+ errors.push(`'${path}.maxConcurrent' must be at least 1`);
295
+ }
296
+ }
297
+
298
+ // autoDiscover
299
+ if ('autoDiscover' in widgetConfig) {
300
+ const err = validateType(widgetConfig.autoDiscover, 'boolean', `${path}.autoDiscover`);
301
+ if (err) errors.push(err);
302
+ }
303
+
304
+ return { errors, warnings };
305
+ }
306
+
307
+ /**
308
+ * Validate a dashboard configuration object
309
+ * @param {Object} config - The parsed configuration object
310
+ * @param {Object} options - Validation options
311
+ * @param {boolean} options.strict - Whether to fail on unknown properties
312
+ * @returns {ConfigValidationResult} Validation result
313
+ */
314
+ export function validateConfig(config, options = {}) {
315
+ const { strict = false } = options;
316
+ const errors = [];
317
+ const warnings = [];
318
+ const info = [];
319
+
320
+ if (!config || typeof config !== 'object') {
321
+ return {
322
+ valid: false,
323
+ errors: ['Config must be a valid JSON object'],
324
+ warnings: [],
325
+ info: [],
326
+ stats: { fieldCount: 0 }
327
+ };
328
+ }
329
+
330
+ const fieldCount = Object.keys(config).length;
331
+
332
+ // Validate refreshInterval
333
+ if ('refreshInterval' in config) {
334
+ const err = validateType(config.refreshInterval, 'integer', 'refreshInterval');
335
+ if (err) errors.push(err);
336
+ else if (config.refreshInterval < VALIDATION.REFRESH_INTERVAL.MIN) {
337
+ errors.push(`'refreshInterval' must be at least ${VALIDATION.REFRESH_INTERVAL.MIN}ms`);
338
+ } else if (config.refreshInterval > VALIDATION.REFRESH_INTERVAL.MAX) {
339
+ errors.push(`'refreshInterval' must be at most ${VALIDATION.REFRESH_INTERVAL.MAX}ms`);
340
+ }
341
+
342
+ // Check if value is in recommended options (1000, 2000, 5000, 10000ms)
343
+ const standardOptions = [1000, 2000, 5000, 10000];
344
+ if (!standardOptions.includes(config.refreshInterval)) {
345
+ const closest = standardOptions.reduce((prev, curr) =>
346
+ Math.abs(curr - config.refreshInterval) < Math.abs(prev - config.refreshInterval) ? curr : prev
347
+ );
348
+ info.push(`'refreshInterval' value ${config.refreshInterval}ms is not standard. Closest: ${closest}ms`);
349
+ }
350
+ }
351
+
352
+ // Validate logLevelFilter
353
+ if ('logLevelFilter' in config) {
354
+ if (!VALIDATION.VALID_LOG_LEVELS.includes(config.logLevelFilter)) {
355
+ errors.push(`'logLevelFilter' must be one of: ${VALIDATION.VALID_LOG_LEVELS.join(', ')}`);
356
+ }
357
+ }
358
+
359
+ // Validate sessionSortMode
360
+ if ('sessionSortMode' in config) {
361
+ if (!VALIDATION.VALID_SORT_MODES.includes(config.sessionSortMode)) {
362
+ errors.push(`'sessionSortMode' must be one of: ${VALIDATION.VALID_SORT_MODES.join(', ')}`);
363
+ }
364
+ }
365
+
366
+ // Validate theme
367
+ if ('theme' in config) {
368
+ if (!VALIDATION.VALID_THEMES.includes(config.theme)) {
369
+ errors.push(`'theme' must be one of: ${VALIDATION.VALID_THEMES.join(', ')}`);
370
+ }
371
+ }
372
+
373
+ // Validate exportFormat
374
+ if ('exportFormat' in config) {
375
+ if (!VALIDATION.VALID_EXPORT_FORMATS.includes(config.exportFormat)) {
376
+ errors.push(`'exportFormat' must be one of: ${VALIDATION.VALID_EXPORT_FORMATS.join(', ')}`);
377
+ }
378
+ }
379
+
380
+ // Validate boolean showWidget fields
381
+ for (let i = 1; i <= 8; i++) {
382
+ const field = `showWidget${i}`;
383
+ if (field in config) {
384
+ const err = validateType(config[field], 'boolean', field);
385
+ if (err) errors.push(err);
386
+ }
387
+ }
388
+
389
+ // Validate showPerformanceMetrics
390
+ if ('showPerformanceMetrics' in config) {
391
+ const err = validateType(config.showPerformanceMetrics, 'boolean', 'showPerformanceMetrics');
392
+ if (err) errors.push(err);
393
+ }
394
+
395
+ // Validate firstRun
396
+ if ('firstRun' in config) {
397
+ const err = validateType(config.firstRun, 'boolean', 'firstRun');
398
+ if (err) errors.push(err);
399
+ }
400
+
401
+ // Validate showFavoritesOnly
402
+ if ('showFavoritesOnly' in config) {
403
+ const err = validateType(config.showFavoritesOnly, 'boolean', 'showFavoritesOnly');
404
+ if (err) errors.push(err);
405
+ }
406
+
407
+ // Validate favorites
408
+ if ('favorites' in config) {
409
+ if (!config.favorites || typeof config.favorites !== 'object') {
410
+ errors.push('\'favorites\' must be an object');
411
+ }
412
+ }
413
+
414
+ // Validate gatewayEndpoints
415
+ if ('gatewayEndpoints' in config) {
416
+ if (!Array.isArray(config.gatewayEndpoints)) {
417
+ errors.push('\'gatewayEndpoints\' must be an array');
418
+ } else {
419
+ if (config.gatewayEndpoints.length === 0) {
420
+ warnings.push('\'gatewayEndpoints\' is empty - no endpoints configured');
421
+ }
422
+ if (config.gatewayEndpoints.length > GATEWAY.MAX_ENDPOINTS) {
423
+ errors.push(`'gatewayEndpoints' exceeds maximum of ${GATEWAY.MAX_ENDPOINTS} endpoints`);
424
+ }
425
+ for (let i = 0; i < config.gatewayEndpoints.length; i++) {
426
+ const result = validateGatewayEndpoint(config.gatewayEndpoints[i], i);
427
+ errors.push(...result.errors);
428
+ warnings.push(...result.warnings);
429
+ }
430
+ }
431
+ }
432
+
433
+ // Validate activeGatewayEndpoint
434
+ if ('activeGatewayEndpoint' in config) {
435
+ const err = validateType(config.activeGatewayEndpoint, 'string', 'activeGatewayEndpoint');
436
+ if (err) errors.push(err);
437
+ }
438
+
439
+ // Validate webInterface
440
+ if ('webInterface' in config) {
441
+ const result = validateWebInterfaceConfig(config.webInterface);
442
+ errors.push(...result.errors);
443
+ warnings.push(...result.warnings);
444
+ }
445
+
446
+ // Validate widgetLoading
447
+ if ('widgetLoading' in config) {
448
+ const result = validateWidgetLoadingConfig(config.widgetLoading);
449
+ errors.push(...result.errors);
450
+ warnings.push(...result.warnings);
451
+ }
452
+
453
+ // Validate plugins
454
+ if ('plugins' in config) {
455
+ if (!config.plugins || typeof config.plugins !== 'object') {
456
+ errors.push('\'plugins\' must be an object');
457
+ } else {
458
+ const pluginCount = Object.keys(config.plugins).length;
459
+ if (pluginCount > 0) {
460
+ info.push(`Found configuration for ${pluginCount} plugin(s)`);
461
+ }
462
+ }
463
+ }
464
+
465
+ // Validate exportDirectory
466
+ if ('exportDirectory' in config) {
467
+ const err = validateType(config.exportDirectory, 'string', 'exportDirectory');
468
+ if (err) errors.push(err);
469
+ }
470
+
471
+ // Validate sessionSearchQuery
472
+ if ('sessionSearchQuery' in config) {
473
+ const err = validateType(config.sessionSearchQuery, 'string', 'sessionSearchQuery');
474
+ if (err) errors.push(err);
475
+ }
476
+
477
+ // Check for unknown properties in strict mode
478
+ if (strict) {
479
+ const knownProps = Object.keys(DEFAULT_SETTINGS);
480
+ for (const key of Object.keys(config)) {
481
+ if (!knownProps.includes(key)) {
482
+ errors.push(`Unknown property: '${key}'`);
483
+ }
484
+ }
485
+ }
486
+
487
+ return {
488
+ valid: errors.length === 0,
489
+ errors,
490
+ warnings,
491
+ info,
492
+ stats: { fieldCount }
493
+ };
494
+ }
495
+
496
+ /**
497
+ * Validate a configuration file
498
+ * @param {string} filePath - Path to the configuration file
499
+ * @param {Object} options - Validation options
500
+ * @returns {ConfigValidationResult} Validation result
501
+ */
502
+ export function validateConfigFile(filePath, options = {}) {
503
+ const resolvedPath = resolve(filePath);
504
+
505
+ if (!existsSync(resolvedPath)) {
506
+ return {
507
+ valid: false,
508
+ errors: [`File not found: ${resolvedPath}`],
509
+ warnings: [],
510
+ info: [],
511
+ stats: { fieldCount: 0 }
512
+ };
513
+ }
514
+
515
+ let config;
516
+ try {
517
+ const content = readFileSync(resolvedPath, 'utf8');
518
+ config = JSON.parse(content);
519
+ } catch (err) {
520
+ return {
521
+ valid: false,
522
+ errors: [`Failed to parse JSON: ${err.message}`],
523
+ warnings: [],
524
+ info: [],
525
+ stats: { fieldCount: 0 }
526
+ };
527
+ }
528
+
529
+ return validateConfig(config, options);
530
+ }
531
+
532
+ /**
533
+ * Format validation results for display
534
+ * @param {ConfigValidationResult} result - The validation result
535
+ * @param {string} configPath - Optional config file path for context
536
+ * @returns {string} Formatted output
537
+ */
538
+ export function formatConfigValidationResult(result, configPath = '') {
539
+ const lines = [];
540
+ const name = configPath ? ` ${configPath} ` : ' ';
541
+
542
+ if (result.valid) {
543
+ lines.push(`✓ Configuration${name}is valid`);
544
+ } else {
545
+ lines.push(`✗ Configuration${name}validation failed`);
546
+ }
547
+
548
+ if (result.stats?.fieldCount !== undefined) {
549
+ lines.push(` ${result.stats.fieldCount} field(s) checked`);
550
+ }
551
+
552
+ if (result.errors.length > 0) {
553
+ lines.push('');
554
+ lines.push('Errors:');
555
+ result.errors.forEach(err => lines.push(` ✗ ${err}`));
556
+ }
557
+
558
+ if (result.warnings.length > 0) {
559
+ lines.push('');
560
+ lines.push('Warnings:');
561
+ result.warnings.forEach(warn => lines.push(` ⚠ ${warn}`));
562
+ }
563
+
564
+ if (result.info.length > 0) {
565
+ lines.push('');
566
+ lines.push('Info:');
567
+ result.info.forEach(i => lines.push(` ℹ ${i}`));
568
+ }
569
+
570
+ return lines.join('\n');
571
+ }
572
+
573
+ /**
574
+ * Get default config path
575
+ * @returns {string} Default configuration file path
576
+ */
577
+ export function getDefaultConfigPath() {
578
+ return PATHS.SETTINGS;
579
+ }
580
+
581
+ export default {
582
+ validateConfig,
583
+ validateConfigFile,
584
+ formatConfigValidationResult,
585
+ getDefaultConfigPath
586
+ };