@superdangerous/app-framework 4.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 (239) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +652 -0
  3. package/dist/api/logsRouter.d.ts +20 -0
  4. package/dist/api/logsRouter.d.ts.map +1 -0
  5. package/dist/api/logsRouter.js +515 -0
  6. package/dist/api/logsRouter.js.map +1 -0
  7. package/dist/cli/dev-server.d.ts +7 -0
  8. package/dist/cli/dev-server.d.ts.map +1 -0
  9. package/dist/cli/dev-server.js +640 -0
  10. package/dist/cli/dev-server.js.map +1 -0
  11. package/dist/cli/index.d.ts +7 -0
  12. package/dist/cli/index.d.ts.map +1 -0
  13. package/dist/cli/index.js +26 -0
  14. package/dist/cli/index.js.map +1 -0
  15. package/dist/core/StandardServer.d.ts +129 -0
  16. package/dist/core/StandardServer.d.ts.map +1 -0
  17. package/dist/core/StandardServer.js +453 -0
  18. package/dist/core/StandardServer.js.map +1 -0
  19. package/dist/core/apiResponse.d.ts +69 -0
  20. package/dist/core/apiResponse.d.ts.map +1 -0
  21. package/dist/core/apiResponse.js +127 -0
  22. package/dist/core/apiResponse.js.map +1 -0
  23. package/dist/core/healthCheck.d.ts +160 -0
  24. package/dist/core/healthCheck.d.ts.map +1 -0
  25. package/dist/core/healthCheck.js +398 -0
  26. package/dist/core/healthCheck.js.map +1 -0
  27. package/dist/core/index.d.ts +40 -0
  28. package/dist/core/index.d.ts.map +1 -0
  29. package/dist/core/index.js +40 -0
  30. package/dist/core/index.js.map +1 -0
  31. package/dist/core/logger.d.ts +117 -0
  32. package/dist/core/logger.d.ts.map +1 -0
  33. package/dist/core/logger.js +826 -0
  34. package/dist/core/logger.js.map +1 -0
  35. package/dist/core/portUtils.d.ts +71 -0
  36. package/dist/core/portUtils.d.ts.map +1 -0
  37. package/dist/core/portUtils.js +240 -0
  38. package/dist/core/portUtils.js.map +1 -0
  39. package/dist/core/storageService.d.ts +119 -0
  40. package/dist/core/storageService.d.ts.map +1 -0
  41. package/dist/core/storageService.js +405 -0
  42. package/dist/core/storageService.js.map +1 -0
  43. package/dist/desktop/bundler.d.ts +40 -0
  44. package/dist/desktop/bundler.d.ts.map +1 -0
  45. package/dist/desktop/bundler.js +176 -0
  46. package/dist/desktop/bundler.js.map +1 -0
  47. package/dist/desktop/index.d.ts +25 -0
  48. package/dist/desktop/index.d.ts.map +1 -0
  49. package/dist/desktop/index.js +15 -0
  50. package/dist/desktop/index.js.map +1 -0
  51. package/dist/desktop/native-modules.d.ts +66 -0
  52. package/dist/desktop/native-modules.d.ts.map +1 -0
  53. package/dist/desktop/native-modules.js +200 -0
  54. package/dist/desktop/native-modules.js.map +1 -0
  55. package/dist/index.d.ts +29 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +39 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/logging/LogCategories.d.ts +87 -0
  60. package/dist/logging/LogCategories.d.ts.map +1 -0
  61. package/dist/logging/LogCategories.js +205 -0
  62. package/dist/logging/LogCategories.js.map +1 -0
  63. package/dist/middleware/aiErrorHandler.d.ts +31 -0
  64. package/dist/middleware/aiErrorHandler.d.ts.map +1 -0
  65. package/dist/middleware/aiErrorHandler.js +181 -0
  66. package/dist/middleware/aiErrorHandler.js.map +1 -0
  67. package/dist/middleware/auth.d.ts +101 -0
  68. package/dist/middleware/auth.d.ts.map +1 -0
  69. package/dist/middleware/auth.js +230 -0
  70. package/dist/middleware/auth.js.map +1 -0
  71. package/dist/middleware/cors.d.ts +56 -0
  72. package/dist/middleware/cors.d.ts.map +1 -0
  73. package/dist/middleware/cors.js +123 -0
  74. package/dist/middleware/cors.js.map +1 -0
  75. package/dist/middleware/errorHandler.d.ts +13 -0
  76. package/dist/middleware/errorHandler.d.ts.map +1 -0
  77. package/dist/middleware/errorHandler.js +85 -0
  78. package/dist/middleware/errorHandler.js.map +1 -0
  79. package/dist/middleware/fileUpload.d.ts +62 -0
  80. package/dist/middleware/fileUpload.d.ts.map +1 -0
  81. package/dist/middleware/fileUpload.js +175 -0
  82. package/dist/middleware/fileUpload.js.map +1 -0
  83. package/dist/middleware/health.d.ts +48 -0
  84. package/dist/middleware/health.d.ts.map +1 -0
  85. package/dist/middleware/health.js +143 -0
  86. package/dist/middleware/health.js.map +1 -0
  87. package/dist/middleware/index.d.ts +20 -0
  88. package/dist/middleware/index.d.ts.map +1 -0
  89. package/dist/middleware/index.js +18 -0
  90. package/dist/middleware/index.js.map +1 -0
  91. package/dist/middleware/openapi.d.ts +64 -0
  92. package/dist/middleware/openapi.d.ts.map +1 -0
  93. package/dist/middleware/openapi.js +258 -0
  94. package/dist/middleware/openapi.js.map +1 -0
  95. package/dist/middleware/requestLogging.d.ts +22 -0
  96. package/dist/middleware/requestLogging.d.ts.map +1 -0
  97. package/dist/middleware/requestLogging.js +61 -0
  98. package/dist/middleware/requestLogging.js.map +1 -0
  99. package/dist/middleware/session.d.ts +84 -0
  100. package/dist/middleware/session.d.ts.map +1 -0
  101. package/dist/middleware/session.js +189 -0
  102. package/dist/middleware/session.js.map +1 -0
  103. package/dist/middleware/validation.d.ts +1337 -0
  104. package/dist/middleware/validation.d.ts.map +1 -0
  105. package/dist/middleware/validation.js +483 -0
  106. package/dist/middleware/validation.js.map +1 -0
  107. package/dist/services/aiService.d.ts +180 -0
  108. package/dist/services/aiService.d.ts.map +1 -0
  109. package/dist/services/aiService.js +547 -0
  110. package/dist/services/aiService.js.map +1 -0
  111. package/dist/services/conversationStorage.d.ts +38 -0
  112. package/dist/services/conversationStorage.d.ts.map +1 -0
  113. package/dist/services/conversationStorage.js +158 -0
  114. package/dist/services/conversationStorage.js.map +1 -0
  115. package/dist/services/crossPlatformBuffer.d.ts +84 -0
  116. package/dist/services/crossPlatformBuffer.d.ts.map +1 -0
  117. package/dist/services/crossPlatformBuffer.js +246 -0
  118. package/dist/services/crossPlatformBuffer.js.map +1 -0
  119. package/dist/services/index.d.ts +17 -0
  120. package/dist/services/index.d.ts.map +1 -0
  121. package/dist/services/index.js +18 -0
  122. package/dist/services/index.js.map +1 -0
  123. package/dist/services/networkService.d.ts +81 -0
  124. package/dist/services/networkService.d.ts.map +1 -0
  125. package/dist/services/networkService.js +268 -0
  126. package/dist/services/networkService.js.map +1 -0
  127. package/dist/services/queueService.d.ts +112 -0
  128. package/dist/services/queueService.d.ts.map +1 -0
  129. package/dist/services/queueService.js +338 -0
  130. package/dist/services/queueService.js.map +1 -0
  131. package/dist/services/settingsService.d.ts +135 -0
  132. package/dist/services/settingsService.d.ts.map +1 -0
  133. package/dist/services/settingsService.js +425 -0
  134. package/dist/services/settingsService.js.map +1 -0
  135. package/dist/services/systemMonitor.d.ts +208 -0
  136. package/dist/services/systemMonitor.d.ts.map +1 -0
  137. package/dist/services/systemMonitor.js +693 -0
  138. package/dist/services/systemMonitor.js.map +1 -0
  139. package/dist/services/updateService.d.ts +78 -0
  140. package/dist/services/updateService.d.ts.map +1 -0
  141. package/dist/services/updateService.js +252 -0
  142. package/dist/services/updateService.js.map +1 -0
  143. package/dist/services/websocketEvents.d.ts +372 -0
  144. package/dist/services/websocketEvents.d.ts.map +1 -0
  145. package/dist/services/websocketEvents.js +338 -0
  146. package/dist/services/websocketEvents.js.map +1 -0
  147. package/dist/services/websocketServer.d.ts +80 -0
  148. package/dist/services/websocketServer.d.ts.map +1 -0
  149. package/dist/services/websocketServer.js +299 -0
  150. package/dist/services/websocketServer.js.map +1 -0
  151. package/dist/settings/SettingsSchema.d.ts +151 -0
  152. package/dist/settings/SettingsSchema.d.ts.map +1 -0
  153. package/dist/settings/SettingsSchema.js +424 -0
  154. package/dist/settings/SettingsSchema.js.map +1 -0
  155. package/dist/testing/TestServer.d.ts +69 -0
  156. package/dist/testing/TestServer.d.ts.map +1 -0
  157. package/dist/testing/TestServer.js +250 -0
  158. package/dist/testing/TestServer.js.map +1 -0
  159. package/dist/types/index.d.ts +137 -0
  160. package/dist/types/index.d.ts.map +1 -0
  161. package/dist/types/index.js +5 -0
  162. package/dist/types/index.js.map +1 -0
  163. package/dist/utils/appPaths.d.ts +74 -0
  164. package/dist/utils/appPaths.d.ts.map +1 -0
  165. package/dist/utils/appPaths.js +162 -0
  166. package/dist/utils/appPaths.js.map +1 -0
  167. package/dist/utils/fs-utils.d.ts +50 -0
  168. package/dist/utils/fs-utils.d.ts.map +1 -0
  169. package/dist/utils/fs-utils.js +114 -0
  170. package/dist/utils/fs-utils.js.map +1 -0
  171. package/dist/utils/index.d.ts +12 -0
  172. package/dist/utils/index.d.ts.map +1 -0
  173. package/dist/utils/index.js +10 -0
  174. package/dist/utils/index.js.map +1 -0
  175. package/dist/utils/standardConfig.d.ts +61 -0
  176. package/dist/utils/standardConfig.d.ts.map +1 -0
  177. package/dist/utils/standardConfig.js +109 -0
  178. package/dist/utils/standardConfig.js.map +1 -0
  179. package/dist/utils/startupBanner.d.ts +34 -0
  180. package/dist/utils/startupBanner.d.ts.map +1 -0
  181. package/dist/utils/startupBanner.js +169 -0
  182. package/dist/utils/startupBanner.js.map +1 -0
  183. package/dist/utils/startupLogger.d.ts +45 -0
  184. package/dist/utils/startupLogger.d.ts.map +1 -0
  185. package/dist/utils/startupLogger.js +200 -0
  186. package/dist/utils/startupLogger.js.map +1 -0
  187. package/package.json +151 -0
  188. package/src/api/logsRouter.ts +600 -0
  189. package/src/cli/dev-server.ts +803 -0
  190. package/src/cli/index.ts +31 -0
  191. package/src/core/StandardServer.ts +587 -0
  192. package/src/core/apiResponse.ts +202 -0
  193. package/src/core/healthCheck.ts +565 -0
  194. package/src/core/index.ts +80 -0
  195. package/src/core/logger.ts +1092 -0
  196. package/src/core/portUtils.ts +319 -0
  197. package/src/core/storageService.ts +595 -0
  198. package/src/desktop/bundler.ts +271 -0
  199. package/src/desktop/index.ts +18 -0
  200. package/src/desktop/native-modules.ts +289 -0
  201. package/src/index.ts +142 -0
  202. package/src/logging/LogCategories.ts +302 -0
  203. package/src/middleware/aiErrorHandler.ts +278 -0
  204. package/src/middleware/auth.ts +329 -0
  205. package/src/middleware/cors.ts +187 -0
  206. package/src/middleware/errorHandler.ts +103 -0
  207. package/src/middleware/fileUpload.ts +252 -0
  208. package/src/middleware/health.ts +206 -0
  209. package/src/middleware/index.ts +71 -0
  210. package/src/middleware/openapi.ts +305 -0
  211. package/src/middleware/requestLogging.ts +92 -0
  212. package/src/middleware/session.ts +238 -0
  213. package/src/middleware/validation.ts +603 -0
  214. package/src/services/aiService.ts +789 -0
  215. package/src/services/conversationStorage.ts +232 -0
  216. package/src/services/crossPlatformBuffer.ts +341 -0
  217. package/src/services/index.ts +47 -0
  218. package/src/services/networkService.ts +351 -0
  219. package/src/services/queueService.ts +446 -0
  220. package/src/services/settingsService.ts +549 -0
  221. package/src/services/systemMonitor.ts +936 -0
  222. package/src/services/updateService.ts +334 -0
  223. package/src/services/websocketEvents.ts +409 -0
  224. package/src/services/websocketServer.ts +394 -0
  225. package/src/settings/SettingsSchema.ts +664 -0
  226. package/src/testing/TestServer.ts +312 -0
  227. package/src/types/index.ts +154 -0
  228. package/src/utils/appPaths.ts +196 -0
  229. package/src/utils/fs-utils.ts +130 -0
  230. package/src/utils/index.ts +15 -0
  231. package/src/utils/standardConfig.ts +178 -0
  232. package/src/utils/startupBanner.ts +287 -0
  233. package/src/utils/startupLogger.ts +268 -0
  234. package/ui/dist/index.d.mts +1221 -0
  235. package/ui/dist/index.d.ts +1221 -0
  236. package/ui/dist/index.js +73 -0
  237. package/ui/dist/index.js.map +1 -0
  238. package/ui/dist/index.mjs +73 -0
  239. package/ui/dist/index.mjs.map +1 -0
@@ -0,0 +1,664 @@
1
+ /**
2
+ * Settings Schema System
3
+ * Consolidated settings system with all features from both implementations
4
+ */
5
+
6
+ export interface SettingOption {
7
+ value: any; // Changed from string to any to support more types
8
+ label: string;
9
+ description?: string;
10
+ }
11
+
12
+ export interface SettingDefinition {
13
+ // Core fields
14
+ key: string;
15
+ label: string;
16
+ description?: string;
17
+ defaultValue?: any;
18
+
19
+ // Type system - comprehensive list
20
+ type:
21
+ | "string"
22
+ | "number"
23
+ | "boolean"
24
+ | "select"
25
+ | "multiselect"
26
+ | "password"
27
+ | "textarea"
28
+ | "email"
29
+ | "url"
30
+ | "color"
31
+ | "date"
32
+ | "time"
33
+ | "datetime"
34
+ | "json"
35
+ | "ipaddress"
36
+ | "network-interface";
37
+
38
+ // Options for select/multiselect
39
+ options?: SettingOption[];
40
+
41
+ // Validation
42
+ required?: boolean;
43
+ validation?: {
44
+ min?: number;
45
+ max?: number;
46
+ minLength?: number;
47
+ maxLength?: number;
48
+ pattern?: string;
49
+ custom?: (value: any) => string | null;
50
+ };
51
+ validationMessage?: string; // Custom validation message
52
+
53
+ // Transforms
54
+ transform?: {
55
+ fromStorage?: (value: any) => any;
56
+ toStorage?: (value: any) => any;
57
+ };
58
+
59
+ // UI hints
60
+ help?: string; // Detailed help text for tooltips
61
+ hint?: string; // Hint text below input
62
+ placeholder?: string;
63
+ prefix?: string; // Text before input
64
+ suffix?: string; // Text after input
65
+ inputWidth?: "small" | "medium" | "large" | "full";
66
+ rows?: number; // For textarea
67
+
68
+ // Behavior
69
+ readOnly?: boolean;
70
+ hidden?: boolean; // For config-only settings
71
+ sensitive?: boolean; // For passwords, API keys, etc.
72
+ requiresRestart?: boolean;
73
+
74
+ // Organization
75
+ category: string;
76
+ subcategory?: string;
77
+ group?: string; // Group related fields together
78
+ order?: number; // Display order within category
79
+ icon?: string; // Icon to display with field
80
+
81
+ // Conditional logic
82
+ showIf?: (settings: Record<string, any>) => boolean;
83
+ confirmMessage?: string; // Confirmation before applying
84
+
85
+ // Number-specific
86
+ min?: number;
87
+ max?: number;
88
+ step?: number;
89
+ }
90
+
91
+ export interface SettingsCategory {
92
+ id: string;
93
+ label: string;
94
+ description?: string;
95
+ icon?: string;
96
+ settings: SettingDefinition[];
97
+ order?: number;
98
+ }
99
+
100
+ export interface SettingsSchema {
101
+ categories: SettingsCategory[];
102
+ version: string;
103
+ onSettingChange?: (
104
+ key: string,
105
+ value: any,
106
+ oldValue: any,
107
+ ) => void | Promise<void>;
108
+ onValidate?: (settings: Record<string, any>) => Record<string, string> | null;
109
+ }
110
+
111
+ export interface SettingsFormState {
112
+ values: Record<string, any>;
113
+ errors: Record<string, string>;
114
+ touched: Record<string, boolean>;
115
+ dirty: Record<string, boolean>;
116
+ isValid: boolean;
117
+ isSubmitting: boolean;
118
+ }
119
+
120
+ export interface SettingsValidationResult {
121
+ isValid: boolean;
122
+ errors: Record<string, string>;
123
+ }
124
+
125
+ /**
126
+ * Built-in validation functions
127
+ */
128
+ export const Validators = {
129
+ required:
130
+ (message = "This field is required") =>
131
+ (value: any) => {
132
+ if (!value || (typeof value === "string" && !value.trim())) {
133
+ return message;
134
+ }
135
+ return null;
136
+ },
137
+
138
+ email:
139
+ (message = "Invalid email address") =>
140
+ (value: string) => {
141
+ if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
142
+ return message;
143
+ }
144
+ return null;
145
+ },
146
+
147
+ url:
148
+ (message = "Invalid URL") =>
149
+ (value: string) => {
150
+ if (value) {
151
+ try {
152
+ new URL(value);
153
+ } catch {
154
+ return message;
155
+ }
156
+ }
157
+ return null;
158
+ },
159
+
160
+ ipAddress:
161
+ (message = "Invalid IP address") =>
162
+ (value: string) => {
163
+ if (value) {
164
+ const parts = value.split(".");
165
+ if (parts.length !== 4) return message;
166
+ for (const part of parts) {
167
+ const num = parseInt(part, 10);
168
+ if (isNaN(num) || num < 0 || num > 255) return message;
169
+ }
170
+ }
171
+ return null;
172
+ },
173
+
174
+ port:
175
+ (message = "Invalid port number") =>
176
+ (value: number) => {
177
+ if (value < 1 || value > 65535) {
178
+ return message;
179
+ }
180
+ return null;
181
+ },
182
+
183
+ range: (min: number, max: number, message?: string) => (value: number) => {
184
+ if (value < min || value > max) {
185
+ return message || `Value must be between ${min} and ${max}`;
186
+ }
187
+ return null;
188
+ },
189
+
190
+ pattern:
191
+ (pattern: RegExp, message = "Invalid format") =>
192
+ (value: string) => {
193
+ if (value && !pattern.test(value)) {
194
+ return message;
195
+ }
196
+ return null;
197
+ },
198
+
199
+ minLength: (min: number, message?: string) => (value: string) => {
200
+ if (value && value.length < min) {
201
+ return message || `Must be at least ${min} characters`;
202
+ }
203
+ return null;
204
+ },
205
+
206
+ maxLength: (max: number, message?: string) => (value: string) => {
207
+ if (value && value.length > max) {
208
+ return message || `Must be at most ${max} characters`;
209
+ }
210
+ return null;
211
+ },
212
+ };
213
+
214
+ /**
215
+ * Helper to create a settings schema
216
+ */
217
+ export function createSettingsSchema(schema: SettingsSchema): SettingsSchema {
218
+ // Sort categories by order
219
+ schema.categories.sort((a, b) => (a.order || 0) - (b.order || 0));
220
+ return schema;
221
+ }
222
+
223
+ /**
224
+ * Get setting by key from categories
225
+ */
226
+ export function getSettingByKey(
227
+ categories: SettingsCategory[],
228
+ key: string,
229
+ ): SettingDefinition | undefined {
230
+ for (const category of categories) {
231
+ const setting = category.settings.find((s) => s.key === key);
232
+ if (setting) return setting;
233
+ }
234
+ return undefined;
235
+ }
236
+
237
+ /**
238
+ * Validate a single setting
239
+ */
240
+ export function validateSetting(
241
+ setting: SettingDefinition,
242
+ value: any,
243
+ ): string | null {
244
+ // Check required
245
+ if (
246
+ setting.required &&
247
+ (!value || (typeof value === "string" && !value.trim()))
248
+ ) {
249
+ return setting.validationMessage || "This field is required";
250
+ }
251
+
252
+ // Type-specific validation
253
+ switch (setting.type) {
254
+ case "string":
255
+ case "password":
256
+ case "textarea":
257
+ if (value && typeof value !== "string") {
258
+ return setting.validationMessage || "Must be a string";
259
+ }
260
+ if (setting.validation) {
261
+ if (
262
+ setting.validation.minLength !== undefined &&
263
+ value.length < setting.validation.minLength
264
+ ) {
265
+ return (
266
+ setting.validationMessage ||
267
+ `Must be at least ${setting.validation.minLength} characters`
268
+ );
269
+ }
270
+ if (
271
+ setting.validation.maxLength !== undefined &&
272
+ value.length > setting.validation.maxLength
273
+ ) {
274
+ return (
275
+ setting.validationMessage ||
276
+ `Must be at most ${setting.validation.maxLength} characters`
277
+ );
278
+ }
279
+ if (
280
+ setting.validation.pattern &&
281
+ !new RegExp(setting.validation.pattern).test(value)
282
+ ) {
283
+ return setting.validationMessage || "Invalid format";
284
+ }
285
+ }
286
+ break;
287
+
288
+ case "email":
289
+ if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
290
+ return setting.validationMessage || "Invalid email address";
291
+ }
292
+ break;
293
+
294
+ case "url":
295
+ try {
296
+ if (value) new URL(value);
297
+ } catch {
298
+ return setting.validationMessage || "Invalid URL";
299
+ }
300
+ break;
301
+
302
+ case "ipaddress":
303
+ if (value) {
304
+ const parts = value.split(".");
305
+ if (parts.length !== 4) {
306
+ return setting.validationMessage || "Invalid IP address";
307
+ }
308
+ for (const part of parts) {
309
+ const num = parseInt(part, 10);
310
+ if (isNaN(num) || num < 0 || num > 255) {
311
+ return setting.validationMessage || "Invalid IP address";
312
+ }
313
+ }
314
+ }
315
+ break;
316
+
317
+ case "number":
318
+ if (typeof value !== "number" || isNaN(value)) {
319
+ return setting.validationMessage || "Must be a valid number";
320
+ }
321
+ if (setting.validation) {
322
+ if (
323
+ setting.validation.min !== undefined &&
324
+ value < setting.validation.min
325
+ ) {
326
+ return (
327
+ setting.validationMessage ||
328
+ `Must be at least ${setting.validation.min}`
329
+ );
330
+ }
331
+ if (
332
+ setting.validation.max !== undefined &&
333
+ value > setting.validation.max
334
+ ) {
335
+ return (
336
+ setting.validationMessage ||
337
+ `Must be at most ${setting.validation.max}`
338
+ );
339
+ }
340
+ }
341
+ // Also check min/max at root level for backward compatibility
342
+ if (setting.min !== undefined && value < setting.min) {
343
+ return setting.validationMessage || `Must be at least ${setting.min}`;
344
+ }
345
+ if (setting.max !== undefined && value > setting.max) {
346
+ return setting.validationMessage || `Must be at most ${setting.max}`;
347
+ }
348
+ break;
349
+
350
+ case "boolean":
351
+ if (typeof value !== "boolean") {
352
+ return setting.validationMessage || "Must be true or false";
353
+ }
354
+ break;
355
+
356
+ case "select":
357
+ case "multiselect":
358
+ if (setting.options) {
359
+ if (
360
+ setting.type === "select" &&
361
+ !setting.options.some((opt) => opt.value === value)
362
+ ) {
363
+ return setting.validationMessage || "Must select a valid option";
364
+ }
365
+ if (setting.type === "multiselect" && Array.isArray(value)) {
366
+ const validValues = setting.options.map((opt) => opt.value);
367
+ if (!value.every((v) => validValues.includes(v))) {
368
+ return setting.validationMessage || "Invalid selection";
369
+ }
370
+ }
371
+ }
372
+ break;
373
+
374
+ case "date":
375
+ case "time":
376
+ case "datetime":
377
+ if (value && !Date.parse(value)) {
378
+ return setting.validationMessage || "Invalid date/time";
379
+ }
380
+ break;
381
+
382
+ case "color":
383
+ if (value && !/^#[0-9A-Fa-f]{6}$/.test(value)) {
384
+ return (
385
+ setting.validationMessage || "Invalid color (must be hex format)"
386
+ );
387
+ }
388
+ break;
389
+ }
390
+
391
+ // Custom validation
392
+ if (setting.validation?.custom) {
393
+ const error = setting.validation.custom(value);
394
+ if (error) return error;
395
+ }
396
+
397
+ return null;
398
+ }
399
+
400
+ /**
401
+ * Validate all settings against schema
402
+ */
403
+ export function validateSettings(
404
+ schema: SettingsSchema,
405
+ settings: Record<string, any>,
406
+ ): Record<string, string> {
407
+ const errors: Record<string, string> = {};
408
+
409
+ for (const category of schema.categories) {
410
+ for (const setting of category.settings) {
411
+ if (setting.hidden) continue;
412
+
413
+ const value = settings[setting.key];
414
+ const error = validateSetting(setting, value);
415
+ if (error) {
416
+ errors[setting.key] = error;
417
+ }
418
+ }
419
+ }
420
+
421
+ // Run custom validation
422
+ if (schema.onValidate) {
423
+ const customErrors = schema.onValidate(settings);
424
+ if (customErrors) {
425
+ Object.assign(errors, customErrors);
426
+ }
427
+ }
428
+
429
+ return errors;
430
+ }
431
+
432
+ /**
433
+ * Validate all settings (returns result object)
434
+ */
435
+ export function validateAllSettings(
436
+ categories: SettingsCategory[],
437
+ values: Record<string, any>,
438
+ ): SettingsValidationResult {
439
+ const errors: Record<string, string> = {};
440
+
441
+ for (const category of categories) {
442
+ for (const setting of category.settings) {
443
+ if (setting.hidden) continue;
444
+
445
+ const value = values[setting.key];
446
+ const error = validateSetting(setting, value);
447
+ if (error) {
448
+ errors[setting.key] = error;
449
+ }
450
+ }
451
+ }
452
+
453
+ return {
454
+ isValid: Object.keys(errors).length === 0,
455
+ errors,
456
+ };
457
+ }
458
+
459
+ /**
460
+ * Helper to flatten nested settings for storage
461
+ */
462
+ export function flattenSettings(
463
+ settings: any,
464
+ prefix = "",
465
+ ): Record<string, any> {
466
+ const flattened: Record<string, any> = {};
467
+
468
+ for (const [key, value] of Object.entries(settings)) {
469
+ const fullKey = prefix ? `${prefix}.${key}` : key;
470
+
471
+ if (
472
+ value &&
473
+ typeof value === "object" &&
474
+ !Array.isArray(value) &&
475
+ !(value instanceof Date)
476
+ ) {
477
+ Object.assign(flattened, flattenSettings(value, fullKey));
478
+ } else {
479
+ flattened[fullKey] = value;
480
+ }
481
+ }
482
+
483
+ return flattened;
484
+ }
485
+
486
+ // Alias for consistency
487
+ export const flattenSettingsValues = flattenSettings;
488
+
489
+ /**
490
+ * Helper to unflatten settings from storage
491
+ */
492
+ export function unflattenSettings(flattened: Record<string, any>): any {
493
+ const settings: any = {};
494
+
495
+ for (const [key, value] of Object.entries(flattened)) {
496
+ const parts = key.split(".");
497
+ let current = settings;
498
+
499
+ for (let i = 0; i < parts.length - 1; i++) {
500
+ const part = parts[i];
501
+ if (!part) continue;
502
+ if (!current[part]) {
503
+ current[part] = {};
504
+ }
505
+ current = current[part];
506
+ }
507
+
508
+ const lastPart = parts[parts.length - 1];
509
+ if (lastPart) {
510
+ current[lastPart] = value;
511
+ }
512
+ }
513
+
514
+ return settings;
515
+ }
516
+
517
+ // Alias for consistency
518
+ export const unflattenSettingsValues = unflattenSettings;
519
+
520
+ /**
521
+ * Get all settings that require restart
522
+ */
523
+ export function getRestartRequiredSettings(
524
+ schema: SettingsSchema | SettingsCategory[],
525
+ changedSettings: Record<string, any> | string[],
526
+ ): string[] {
527
+ const restartRequired: string[] = [];
528
+
529
+ // Handle both schema and categories array
530
+ const categories = Array.isArray(schema) ? schema : schema.categories;
531
+
532
+ // Handle both changed settings object and array of keys
533
+ const changedKeys = Array.isArray(changedSettings)
534
+ ? changedSettings
535
+ : Object.keys(changedSettings);
536
+
537
+ for (const category of categories) {
538
+ for (const setting of category.settings) {
539
+ if (setting.requiresRestart && changedKeys.includes(setting.key)) {
540
+ restartRequired.push(setting.key);
541
+ }
542
+ }
543
+ }
544
+
545
+ return restartRequired;
546
+ }
547
+
548
+ /**
549
+ * Get default values from settings
550
+ */
551
+ export function getDefaultSettingsValues(
552
+ categories: SettingsCategory[],
553
+ ): Record<string, any> {
554
+ const defaults: Record<string, any> = {};
555
+
556
+ for (const category of categories) {
557
+ for (const setting of category.settings) {
558
+ if (!setting.hidden && setting.defaultValue !== undefined) {
559
+ defaults[setting.key] = setting.defaultValue;
560
+ }
561
+ }
562
+ }
563
+
564
+ return defaults;
565
+ }
566
+
567
+ /**
568
+ * Evaluate conditional visibility for a field
569
+ */
570
+ export function evaluateFieldVisibility(
571
+ field: SettingDefinition,
572
+ currentValues: Record<string, any>,
573
+ ): boolean {
574
+ if (!field.showIf) {
575
+ return true;
576
+ }
577
+
578
+ try {
579
+ return field.showIf(currentValues);
580
+ } catch (_error) {
581
+ console.error(`Error evaluating showIf for field ${field.key}:`, _error);
582
+ return true; // Show field if evaluation fails
583
+ }
584
+ }
585
+
586
+ /**
587
+ * Group fields by their group property
588
+ */
589
+ export function groupFields(
590
+ fields: SettingDefinition[],
591
+ ): Map<string, SettingDefinition[]> {
592
+ const groups = new Map<string, SettingDefinition[]>();
593
+
594
+ // First, add ungrouped fields
595
+ const ungrouped = fields.filter((f) => !f.group);
596
+ if (ungrouped.length > 0) {
597
+ groups.set("_default", ungrouped);
598
+ }
599
+
600
+ // Then, group the rest
601
+ fields
602
+ .filter((f) => f.group)
603
+ .forEach((field) => {
604
+ const group = field.group!;
605
+ if (!groups.has(group)) {
606
+ groups.set(group, []);
607
+ }
608
+ groups.get(group)!.push(field);
609
+ });
610
+
611
+ // Sort fields within each group by order
612
+ groups.forEach((groupFields) => {
613
+ groupFields.sort((a, b) => (a.order || 999) - (b.order || 999));
614
+ });
615
+
616
+ return groups;
617
+ }
618
+
619
+ /**
620
+ * Apply toStorage transform
621
+ */
622
+ export function applyToStorageTransform(
623
+ field: SettingDefinition,
624
+ value: any,
625
+ ): any {
626
+ if (field.transform?.toStorage) {
627
+ return field.transform.toStorage(value);
628
+ }
629
+ return value;
630
+ }
631
+
632
+ /**
633
+ * Apply fromStorage transform
634
+ */
635
+ export function applyFromStorageTransform(
636
+ field: SettingDefinition,
637
+ value: any,
638
+ ): any {
639
+ if (field.transform?.fromStorage) {
640
+ return field.transform.fromStorage(value);
641
+ }
642
+ return value;
643
+ }
644
+
645
+ /**
646
+ * Create settings form state
647
+ */
648
+ export function createSettingsFormState(
649
+ categories: SettingsCategory[],
650
+ initialValues?: Record<string, any>,
651
+ ): SettingsFormState {
652
+ const defaults = getDefaultSettingsValues(categories);
653
+ const values = { ...defaults, ...initialValues };
654
+ const validation = validateAllSettings(categories, values);
655
+
656
+ return {
657
+ values,
658
+ errors: validation.errors,
659
+ touched: {},
660
+ dirty: {},
661
+ isValid: validation.isValid,
662
+ isSubmitting: false,
663
+ };
664
+ }