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,596 @@
1
+ /**
2
+ * Widget Dependency Resolver
3
+ * Provides topological sorting, circular dependency detection, and constraint checking
4
+ */
5
+
6
+ /**
7
+ * Represents a dependency with optional constraints
8
+ * @typedef {Object} Dependency
9
+ * @property {string} id - Widget ID that is depended on
10
+ * @property {boolean} [optional=false] - Whether this dependency is optional
11
+ * @property {string} [version] - Optional version constraint (semver range)
12
+ */
13
+
14
+ /**
15
+ * Result of dependency resolution
16
+ * @typedef {Object} ResolutionResult
17
+ * @property {boolean} success - Whether resolution succeeded
18
+ * @property {string[]} order - Topologically sorted widget IDs in load order
19
+ * @property {string} [error] - Error message if resolution failed
20
+ * @property {string[]} [circularPath] - Path of circular dependency if detected
21
+ * @property {Object} [missingDeps] - Map of widget ID to missing dependency IDs
22
+ * @property {Object} [constraintViolations] - Map of widget ID to constraint violations
23
+ */
24
+
25
+ /**
26
+ * Graph node representing a widget and its dependencies
27
+ * @typedef {Object} DependencyNode
28
+ * @property {string} id - Widget ID
29
+ * @property {Dependency[]} dependencies - Parsed dependencies
30
+ * @property {number} inDegree - Number of incoming edges (for Kahn's algorithm)
31
+ * @property {Set<string>} dependents - Widgets that depend on this one
32
+ */
33
+
34
+ /**
35
+ * Parse a dependency specification which can be:
36
+ * - A simple string (widget ID)
37
+ * - An object with { id, optional, version }
38
+ * @param {string|Object} dep - Dependency specification
39
+ * @returns {Dependency} Parsed dependency object
40
+ */
41
+ export function parseDependency(dep) {
42
+ if (typeof dep === 'string') {
43
+ return { id: dep, optional: false };
44
+ }
45
+
46
+ if (typeof dep === 'object' && dep !== null) {
47
+ if (!dep.id || typeof dep.id !== 'string') {
48
+ throw new Error('Dependency object must have a string "id" property');
49
+ }
50
+ return {
51
+ id: dep.id,
52
+ optional: dep.optional === true,
53
+ version: dep.version,
54
+ };
55
+ }
56
+
57
+ throw new Error('Dependency must be a string or an object with an "id" property');
58
+ }
59
+
60
+ /**
61
+ * Parse all dependencies from a widget's metadata
62
+ * @param {Object} metadata - Widget metadata
63
+ * @returns {Dependency[]} Array of parsed dependencies
64
+ */
65
+ export function parseDependencies(metadata) {
66
+ if (!metadata.dependencies || !Array.isArray(metadata.dependencies)) {
67
+ return [];
68
+ }
69
+
70
+ const deps = [];
71
+ for (const dep of metadata.dependencies) {
72
+ try {
73
+ deps.push(parseDependency(dep));
74
+ } catch (err) {
75
+ // Log warning but don't fail - skip invalid dependencies
76
+ console.warn(`Invalid dependency format in widget "${metadata.id || 'unknown'}": ${err.message}`);
77
+ }
78
+ }
79
+ return deps;
80
+ }
81
+
82
+ /**
83
+ * Build a dependency graph from widget registry
84
+ * @param {Map<string, Object>} registry - Widget registry
85
+ * @returns {Map<string, DependencyNode>} Dependency graph
86
+ */
87
+ export function buildDependencyGraph(registry) {
88
+ const graph = new Map();
89
+
90
+ // Create nodes for all registered widgets
91
+ for (const [id, widget] of registry) {
92
+ const deps = parseDependencies(widget.metadata || {});
93
+ graph.set(id, {
94
+ id,
95
+ dependencies: deps,
96
+ inDegree: 0,
97
+ dependents: new Set(),
98
+ });
99
+ }
100
+
101
+ // Build edges and calculate in-degrees
102
+ for (const [id, node] of graph) {
103
+ for (const dep of node.dependencies) {
104
+ const depNode = graph.get(dep.id);
105
+ if (depNode) {
106
+ depNode.dependents.add(id);
107
+ node.inDegree++;
108
+ }
109
+ }
110
+ }
111
+
112
+ return graph;
113
+ }
114
+
115
+ /**
116
+ * Detect circular dependencies using DFS
117
+ * @param {Map<string, DependencyNode>} graph - Dependency graph
118
+ * @returns {string[]|null} Circular path if found, null otherwise
119
+ */
120
+ export function detectCircularDependency(graph) {
121
+ const visited = new Set();
122
+ const recStack = new Set();
123
+ const path = [];
124
+
125
+ function dfs(nodeId) {
126
+ visited.add(nodeId);
127
+ recStack.add(nodeId);
128
+ path.push(nodeId);
129
+
130
+ const node = graph.get(nodeId);
131
+ if (node) {
132
+ for (const dep of node.dependencies) {
133
+ const depId = dep.id;
134
+
135
+ if (!visited.has(depId)) {
136
+ const cycle = dfs(depId);
137
+ if (cycle) return cycle;
138
+ } else if (recStack.has(depId)) {
139
+ // Found a cycle - extract the circular portion of the path
140
+ const cycleStart = path.indexOf(depId);
141
+ return [...path.slice(cycleStart), depId];
142
+ }
143
+ }
144
+ }
145
+
146
+ path.pop();
147
+ recStack.delete(nodeId);
148
+ return null;
149
+ }
150
+
151
+ for (const [id] of graph) {
152
+ if (!visited.has(id)) {
153
+ const cycle = dfs(id);
154
+ if (cycle) return cycle;
155
+ }
156
+ }
157
+
158
+ return null;
159
+ }
160
+
161
+ /**
162
+ * Check if a version satisfies a constraint (basic semver range support)
163
+ * Supported formats:
164
+ * - "1.0.0" - exact version
165
+ * - ">=1.0.0" - greater than or equal
166
+ * - "^1.0.0" - compatible with (same major)
167
+ * - "~1.0.0" - approximately equivalent (same major.minor)
168
+ * @param {string} version - Actual version
169
+ * @param {string} constraint - Version constraint
170
+ * @returns {boolean} Whether version satisfies constraint
171
+ */
172
+ export function satisfiesVersion(version, constraint) {
173
+ if (!version || !constraint) return true;
174
+
175
+ const parseVersion = (v) => {
176
+ const parts = v.replace(/^[=v]+/, '').split('.').map(Number);
177
+ return {
178
+ major: parts[0] || 0,
179
+ minor: parts[1] || 0,
180
+ patch: parts[2] || 0,
181
+ };
182
+ };
183
+
184
+ const v = parseVersion(version);
185
+ const c = parseVersion(constraint.replace(/^[>=^~]+/, ''));
186
+
187
+ // Handle different constraint types
188
+ if (constraint.startsWith('>=')) {
189
+ if (v.major < c.major) return false;
190
+ if (v.major === c.major && v.minor < c.minor) return false;
191
+ if (v.major === c.major && v.minor === c.minor && v.patch < c.patch) return false;
192
+ return true;
193
+ }
194
+
195
+ if (constraint.startsWith('^')) {
196
+ // Compatible with major version
197
+ if (v.major !== c.major) return false;
198
+ if (v.major === 0) {
199
+ // For 0.x.x, minor must match too
200
+ if (v.minor < c.minor) return false;
201
+ if (v.minor === c.minor && v.patch < c.patch) return false;
202
+ }
203
+ return true;
204
+ }
205
+
206
+ if (constraint.startsWith('~')) {
207
+ // Approximately equivalent (major.minor must match exactly)
208
+ if (v.major !== c.major) return false;
209
+ if (v.minor !== c.minor) return false;
210
+ if (v.patch < c.patch) return false;
211
+ return true;
212
+ }
213
+
214
+ // Exact version match
215
+ return v.major === c.major && v.minor === c.minor && v.patch === c.patch;
216
+ }
217
+
218
+ /**
219
+ * Check version constraints for all dependencies
220
+ * @param {Map<string, DependencyNode>} graph - Dependency graph
221
+ * @param {Map<string, Object>} registry - Widget registry
222
+ * @returns {Object|null} Constraint violations map or null if all satisfied
223
+ */
224
+ export function checkVersionConstraints(graph, registry) {
225
+ const violations = {};
226
+
227
+ for (const [id, node] of graph) {
228
+ const widgetViolations = [];
229
+
230
+ for (const dep of node.dependencies) {
231
+ if (!dep.version) continue;
232
+
233
+ const depWidget = registry.get(dep.id);
234
+ if (!depWidget) continue; // Missing dependency - handled separately
235
+
236
+ const depVersion = depWidget.metadata?.version;
237
+ if (!depVersion) {
238
+ widgetViolations.push({
239
+ dependency: dep.id,
240
+ constraint: dep.version,
241
+ actual: 'unknown',
242
+ reason: 'Dependency has no version specified',
243
+ });
244
+ } else if (!satisfiesVersion(depVersion, dep.version)) {
245
+ widgetViolations.push({
246
+ dependency: dep.id,
247
+ constraint: dep.version,
248
+ actual: depVersion,
249
+ reason: `Version ${depVersion} does not satisfy constraint ${dep.version}`,
250
+ });
251
+ }
252
+ }
253
+
254
+ if (widgetViolations.length > 0) {
255
+ violations[id] = widgetViolations;
256
+ }
257
+ }
258
+
259
+ return Object.keys(violations).length > 0 ? violations : null;
260
+ }
261
+
262
+ /**
263
+ * Find missing dependencies (non-optional ones)
264
+ * @param {Map<string, DependencyNode>} graph - Dependency graph
265
+ * @param {Map<string, Object>} registry - Widget registry
266
+ * @returns {Object|null} Map of widget ID to missing dependency IDs
267
+ */
268
+ export function findMissingDependencies(graph, registry) {
269
+ const missing = {};
270
+
271
+ for (const [id, node] of graph) {
272
+ const missingDeps = [];
273
+
274
+ for (const dep of node.dependencies) {
275
+ if (!dep.optional && !registry.has(dep.id)) {
276
+ missingDeps.push(dep.id);
277
+ }
278
+ }
279
+
280
+ if (missingDeps.length > 0) {
281
+ missing[id] = missingDeps;
282
+ }
283
+ }
284
+
285
+ return Object.keys(missing).length > 0 ? missing : null;
286
+ }
287
+
288
+ /**
289
+ * Topological sort using Kahn's algorithm
290
+ * @param {Map<string, DependencyNode>} graph - Dependency graph
291
+ * @param {string[]} [targetIds] - Specific widgets to sort (optional, sorts all if omitted)
292
+ * @returns {string[]} Topologically sorted widget IDs
293
+ */
294
+ export function topologicalSort(graph, targetIds = null) {
295
+ // Create a copy of in-degrees since we'll modify them
296
+ const inDegrees = new Map();
297
+ for (const [id, node] of graph) {
298
+ inDegrees.set(id, node.inDegree);
299
+ }
300
+
301
+ // If targetIds specified, only include those and their dependencies
302
+ const includeSet = targetIds ? new Set(targetIds) : null;
303
+ if (includeSet) {
304
+ // Add all dependencies of target widgets to include set
305
+ const queue = [...targetIds];
306
+ const visited = new Set();
307
+
308
+ for (const id of queue) {
309
+ if (visited.has(id)) continue;
310
+ visited.add(id);
311
+
312
+ const node = graph.get(id);
313
+ if (node) {
314
+ for (const dep of node.dependencies) {
315
+ if (graph.has(dep.id)) {
316
+ includeSet.add(dep.id);
317
+ queue.push(dep.id);
318
+ }
319
+ }
320
+ }
321
+ }
322
+ }
323
+
324
+ // Find all nodes with in-degree 0
325
+ const queue = [];
326
+ for (const [id, degree] of inDegrees) {
327
+ if (degree === 0 && (!includeSet || includeSet.has(id))) {
328
+ queue.push(id);
329
+ }
330
+ }
331
+
332
+ // Sort queue for deterministic order (alphabetical)
333
+ queue.sort();
334
+
335
+ const result = [];
336
+
337
+ while (queue.length > 0) {
338
+ const id = queue.shift();
339
+ result.push(id);
340
+
341
+ const node = graph.get(id);
342
+ if (node) {
343
+ for (const dependentId of node.dependents) {
344
+ if (includeSet && !includeSet.has(dependentId)) continue;
345
+
346
+ const newDegree = inDegrees.get(dependentId) - 1;
347
+ inDegrees.set(dependentId, newDegree);
348
+
349
+ if (newDegree === 0) {
350
+ // Insert in sorted position for deterministic order
351
+ const insertIndex = queue.findIndex(x => x > dependentId);
352
+ if (insertIndex === -1) {
353
+ queue.push(dependentId);
354
+ } else {
355
+ queue.splice(insertIndex, 0, dependentId);
356
+ }
357
+ }
358
+ }
359
+ }
360
+ }
361
+
362
+ return result;
363
+ }
364
+
365
+ /**
366
+ * Resolve dependencies and return load order
367
+ * @param {Map<string, Object>} registry - Widget registry
368
+ * @param {Object} options - Resolution options
369
+ * @param {string[]} [options.targetIds] - Specific widgets to resolve (default: all)
370
+ * @param {boolean} [options.skipVersionCheck=false] - Skip version constraint checking
371
+ * @param {boolean} [options.allowPartial=false] - Allow partial resolution (skip widgets with missing deps)
372
+ * @returns {ResolutionResult} Resolution result with load order or error
373
+ */
374
+ export function resolveDependencies(registry, options = {}) {
375
+ const { targetIds = null, skipVersionCheck = false, allowPartial = false } = options;
376
+
377
+ // Handle empty registry
378
+ if (registry.size === 0) {
379
+ return {
380
+ success: true,
381
+ order: [],
382
+ };
383
+ }
384
+
385
+ // Build dependency graph
386
+ const graph = buildDependencyGraph(registry);
387
+
388
+ // Detect circular dependencies
389
+ const circularPath = detectCircularDependency(graph);
390
+ if (circularPath) {
391
+ return {
392
+ success: false,
393
+ order: [],
394
+ error: `Circular dependency detected: ${circularPath.join(' -> ')}`,
395
+ circularPath,
396
+ };
397
+ }
398
+
399
+ // Check for missing dependencies
400
+ const missingDeps = findMissingDependencies(graph, registry);
401
+ if (missingDeps && !allowPartial) {
402
+ const details = Object.entries(missingDeps)
403
+ .map(([id, deps]) => `"${id}" requires: ${deps.join(', ')}`)
404
+ .join('; ');
405
+
406
+ return {
407
+ success: false,
408
+ order: [],
409
+ error: `Missing required dependencies: ${details}`,
410
+ missingDeps,
411
+ };
412
+ }
413
+
414
+ // Check version constraints
415
+ if (!skipVersionCheck) {
416
+ const violations = checkVersionConstraints(graph, registry);
417
+ if (violations) {
418
+ const details = Object.entries(violations)
419
+ .map(([id, v]) => `"${id}": ${v.map(x => x.reason).join(', ')}`)
420
+ .join('; ');
421
+
422
+ return {
423
+ success: false,
424
+ order: [],
425
+ error: `Version constraint violations: ${details}`,
426
+ constraintViolations: violations,
427
+ };
428
+ }
429
+ }
430
+
431
+ // Get topological sort
432
+ const idsToSort = targetIds || Array.from(registry.keys());
433
+ const order = topologicalSort(graph, idsToSort);
434
+
435
+ // If allowPartial, filter out widgets with missing dependencies
436
+ let finalOrder = order;
437
+ if (allowPartial && missingDeps) {
438
+ const widgetsWithMissingDeps = new Set(Object.keys(missingDeps));
439
+ finalOrder = order.filter(id => !widgetsWithMissingDeps.has(id));
440
+ }
441
+
442
+ return {
443
+ success: true,
444
+ order: finalOrder,
445
+ ...(missingDeps && { missingDeps }),
446
+ };
447
+ }
448
+
449
+ /**
450
+ * Get all dependencies (direct and transitive) for a widget
451
+ * @param {Map<string, DependencyNode>} graph - Dependency graph
452
+ * @param {string} widgetId - Widget ID
453
+ * @param {Object} options - Options
454
+ * @param {boolean} [options.includeOptional=true] - Include optional dependencies
455
+ * @returns {string[]} Array of dependency IDs in dependency order
456
+ */
457
+ export function getAllDependencies(graph, widgetId, options = {}) {
458
+ const { includeOptional = true } = options;
459
+ const deps = new Set();
460
+ const visited = new Set();
461
+
462
+ function collect(id) {
463
+ if (visited.has(id)) return;
464
+ visited.add(id);
465
+
466
+ const node = graph.get(id);
467
+ if (!node) return;
468
+
469
+ for (const dep of node.dependencies) {
470
+ if (!includeOptional && dep.optional) continue;
471
+
472
+ deps.add(dep.id);
473
+ collect(dep.id);
474
+ }
475
+ }
476
+
477
+ collect(widgetId);
478
+ return Array.from(deps);
479
+ }
480
+
481
+ /**
482
+ * Get all widgets that depend on a given widget (direct and indirect)
483
+ * @param {Map<string, DependencyNode>} graph - Dependency graph
484
+ * @param {string} widgetId - Widget ID
485
+ * @returns {string[]} Array of dependent widget IDs
486
+ */
487
+ export function getAllDependents(graph, widgetId) {
488
+ const dependents = new Set();
489
+ const visited = new Set();
490
+
491
+ function collect(id) {
492
+ if (visited.has(id)) return;
493
+ visited.add(id);
494
+
495
+ const node = graph.get(id);
496
+ if (!node) return;
497
+
498
+ for (const depId of node.dependents) {
499
+ dependents.add(depId);
500
+ collect(depId);
501
+ }
502
+ }
503
+
504
+ collect(widgetId);
505
+ return Array.from(dependents);
506
+ }
507
+
508
+ /**
509
+ * Check if loading a widget would violate any dependency constraints
510
+ * @param {Map<string, Object>} registry - Widget registry
511
+ * @param {string} widgetId - Widget to check
512
+ * @returns {Object} Validation result
513
+ */
514
+ export function validateWidgetDependencies(registry, widgetId) {
515
+ const graph = buildDependencyGraph(registry);
516
+ const node = graph.get(widgetId);
517
+
518
+ if (!node) {
519
+ return {
520
+ valid: false,
521
+ error: `Widget "${widgetId}" not found in registry`,
522
+ };
523
+ }
524
+
525
+ // Check for missing dependencies
526
+ const missing = [];
527
+ for (const dep of node.dependencies) {
528
+ if (!dep.optional && !registry.has(dep.id)) {
529
+ missing.push(dep.id);
530
+ }
531
+ }
532
+
533
+ if (missing.length > 0) {
534
+ return {
535
+ valid: false,
536
+ error: `Missing required dependencies: ${missing.join(', ')}`,
537
+ missing,
538
+ };
539
+ }
540
+
541
+ // Check for circular dependencies if this widget were loaded
542
+ const circularPath = detectCircularDependency(graph);
543
+ if (circularPath && circularPath.includes(widgetId)) {
544
+ return {
545
+ valid: false,
546
+ error: `Circular dependency detected: ${circularPath.join(' -> ')}`,
547
+ circularPath,
548
+ };
549
+ }
550
+
551
+ // Check version constraints
552
+ for (const dep of node.dependencies) {
553
+ if (!dep.version) continue;
554
+
555
+ const depWidget = registry.get(dep.id);
556
+ if (!depWidget) continue;
557
+
558
+ const depVersion = depWidget.metadata?.version;
559
+ if (!depVersion) {
560
+ return {
561
+ valid: false,
562
+ error: `Dependency "${dep.id}" has no version for constraint "${dep.version}"`,
563
+ constraintViolation: { dependency: dep.id, constraint: dep.version, actual: null },
564
+ };
565
+ }
566
+
567
+ if (!satisfiesVersion(depVersion, dep.version)) {
568
+ return {
569
+ valid: false,
570
+ error: `Dependency "${dep.id}" version ${depVersion} does not satisfy constraint ${dep.version}`,
571
+ constraintViolation: { dependency: dep.id, constraint: dep.version, actual: depVersion },
572
+ };
573
+ }
574
+ }
575
+
576
+ return {
577
+ valid: true,
578
+ dependencies: node.dependencies.map(d => d.id),
579
+ allDependencies: getAllDependencies(graph, widgetId),
580
+ };
581
+ }
582
+
583
+ export default {
584
+ parseDependency,
585
+ parseDependencies,
586
+ buildDependencyGraph,
587
+ detectCircularDependency,
588
+ satisfiesVersion,
589
+ checkVersionConstraints,
590
+ findMissingDependencies,
591
+ topologicalSort,
592
+ resolveDependencies,
593
+ getAllDependencies,
594
+ getAllDependents,
595
+ validateWidgetDependencies,
596
+ };
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Widget System for Claw Dashboard
3
+ * Provides lazy loading, plugin API, and built-in widgets
4
+ */
5
+
6
+ export { WidgetLoader, getWidgetLoader } from './widget-loader.js';
7
+ export {
8
+ PluginAPI,
9
+ BaseWidget,
10
+ validateManifest,
11
+ createWidgetPlugin,
12
+ PLUGIN_API_VERSION,
13
+ getPluginAPI,
14
+ } from './plugin-api.js';
15
+ export {
16
+ processWidgetConfig,
17
+ interpolateEnvVars,
18
+ processConfigValues,
19
+ validateConfigVersion,
20
+ migrateConfig,
21
+ registerMigration,
22
+ compareVersions,
23
+ extractEnvRequirements,
24
+ createConfigPreprocessor,
25
+ CONFIG_VERSION,
26
+ DEFAULT_PROCESSING_OPTIONS,
27
+ } from './config-processor.js';
28
+
29
+ export {
30
+ parseDependency,
31
+ parseDependencies,
32
+ buildDependencyGraph,
33
+ detectCircularDependency,
34
+ satisfiesVersion,
35
+ checkVersionConstraints,
36
+ findMissingDependencies,
37
+ topologicalSort,
38
+ resolveDependencies,
39
+ getAllDependencies,
40
+ getAllDependents,
41
+ validateWidgetDependencies,
42
+ } from './dependency-resolver.js';
43
+
44
+ export {
45
+ CpuWidget,
46
+ MemoryWidget,
47
+ GpuWidget,
48
+ NetworkWidget,
49
+ DiskWidget,
50
+ SystemWidget,
51
+ UptimeWidget,
52
+ DataHealthWidget,
53
+ SettingsWidget,
54
+ createWidget,
55
+ getWidgetTypes,
56
+ WIDGET_REGISTRY,
57
+ } from './builtin-widgets.js';
58
+
59
+ export {
60
+ WidgetErrorBoundary,
61
+ ErrorBoundaryManager,
62
+ ErrorStyles,
63
+ withErrorBoundary,
64
+ getErrorBoundaryManager,
65
+ } from './widget-error-boundary.js';
66
+
67
+ export {
68
+ PluginError,
69
+ PluginErrorAnalyzer,
70
+ PLUGIN_ERROR_CODES,
71
+ formatPluginError,
72
+ extractErrorInfo,
73
+ } from '../plugin-errors.js';
74
+
75
+ // Re-export all types (including RateLimiter from plugin-api.js)
76
+ export * from './widget-loader.js';
77
+ export * from './plugin-api.js';
78
+ export * from './builtin-widgets.js';
79
+ export * from './widget-error-boundary.js';