claw-dashboard 1.9.0 → 2.0.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/.c8rc.json +38 -0
- package/.dockerignore +68 -0
- package/.github/dependabot.yml +45 -0
- package/.github/pull_request_template.md +39 -0
- package/.github/workflows/ci.yml +147 -0
- package/.github/workflows/release.yml +40 -0
- package/.github/workflows/security.yml +84 -0
- package/.husky/pre-commit +1 -0
- package/.planning/codebase/ARCHITECTURE.md +206 -0
- package/.planning/codebase/INTEGRATIONS.md +150 -0
- package/.planning/codebase/STACK.md +122 -0
- package/.planning/codebase/STRUCTURE.md +201 -0
- package/CHANGELOG.md +293 -0
- package/CONTRIBUTING.md +378 -0
- package/Dockerfile +54 -0
- package/FEATURES.md +195 -21
- package/README.md +83 -6
- package/TODO.md +28 -0
- package/build-cjs.js +127 -0
- package/cjs-shim.js +13 -0
- package/dist/clawdash +1729 -0
- package/dist/clawdash.meta.json +2236 -0
- package/dist/widgets.cjs +6511 -0
- package/docker-compose.yml +67 -0
- package/docs/API.md +1050 -0
- package/docs/PLUGINS.md +1504 -0
- package/esbuild.config.js +158 -0
- package/eslint.config.js +56 -0
- package/examples/plugins/README.md +122 -0
- package/examples/plugins/api-status/index.js +294 -0
- package/examples/plugins/api-status/plugin.json +19 -0
- package/examples/plugins/hello-world/index.js +104 -0
- package/examples/plugins/hello-world/plugin.json +15 -0
- package/examples/plugins/system-metrics-chart/index.js +339 -0
- package/examples/plugins/system-metrics-chart/plugin.json +18 -0
- package/examples/plugins/weather-widget/index.js +136 -0
- package/examples/plugins/weather-widget/plugin.json +19 -0
- package/index.cjs +23005 -0
- package/index.js +5236 -566
- package/jest.config.js +11 -0
- package/man/clawdash.1 +276 -0
- package/package.json +54 -5
- package/schemas/plugin-manifest.json +128 -0
- package/scripts/release.js +595 -0
- package/src/alerts.js +693 -0
- package/src/auto-save.js +584 -0
- package/src/cache.js +390 -0
- package/src/checksum.js +146 -0
- package/src/cli/args.js +110 -0
- package/src/cli/export-schedule.js +423 -0
- package/src/cli/export-snapshot.js +138 -0
- package/src/cli/help.js +69 -0
- package/src/cli/import-snapshot.js +230 -0
- package/src/cli/index.js +14 -0
- package/src/cli/list-templates.js +69 -0
- package/src/cli/validate-config.js +76 -0
- package/src/cli/validate-plugin.js +187 -0
- package/src/cli/version.js +15 -0
- package/src/config-validator.js +586 -0
- package/src/config-watcher.js +401 -0
- package/src/config.js +684 -0
- package/src/container-detector.js +499 -0
- package/src/database.js +734 -0
- package/src/differential-render.js +327 -0
- package/src/errors.js +169 -0
- package/src/export-scheduler.js +581 -0
- package/src/gateway-manager.js +685 -0
- package/src/hints.js +371 -0
- package/src/loading-states.js +509 -0
- package/src/logger.js +185 -0
- package/src/memory-pressure.js +467 -0
- package/src/performance-monitor.js +374 -0
- package/src/plugin-errors.js +586 -0
- package/src/plugin-manifest-validator.js +219 -0
- package/src/plugin-reload.js +481 -0
- package/src/plugin-scaffold.js +1941 -0
- package/src/plugin-validator.js +284 -0
- package/src/retry.js +218 -0
- package/src/security.js +860 -0
- package/src/snapshot.js +372 -0
- package/src/splash.js +115 -0
- package/src/theme-selector.js +411 -0
- package/src/themes.js +874 -0
- package/src/transitions.js +534 -0
- package/src/types.d.ts +174 -0
- package/src/validation.js +926 -0
- package/src/web-server.js +885 -0
- package/src/widgets/builtin-widgets.js +1057 -0
- package/src/widgets/config-processor.js +401 -0
- package/src/widgets/dependency-resolver.js +596 -0
- package/src/widgets/index.js +79 -0
- package/src/widgets/plugin-api.js +845 -0
- package/src/widgets/widget-error-boundary.js +773 -0
- package/src/widgets/widget-error-isolation.js +551 -0
- package/src/widgets/widget-loader.js +1437 -0
- package/src/workers/system-worker.js +130 -0
- package/src/workers/worker-pool.js +633 -0
- package/tests/alerts.test.js +437 -0
- package/tests/auto-save.test.js +529 -0
- package/tests/cache.test.js +317 -0
- package/tests/cli.test.js +351 -0
- package/tests/command-palette.test.js +320 -0
- package/tests/config-processor.test.js +452 -0
- package/tests/config-validator.test.js +710 -0
- package/tests/config-watcher.test.js +594 -0
- package/tests/config.test.js +755 -0
- package/tests/database.test.js +438 -0
- package/tests/errors.test.js +189 -0
- package/tests/example-plugins.test.js +624 -0
- package/tests/integration.test.js +1321 -0
- package/tests/loading-states.test.js +300 -0
- package/tests/manifest-validation-on-load.test.js +671 -0
- package/tests/performance-monitor.test.js +190 -0
- package/tests/plugin-api-rate-limit.test.js +302 -0
- package/tests/plugin-errors.test.js +311 -0
- package/tests/plugin-lifecycle-e2e.test.js +1036 -0
- package/tests/plugin-reload.test.js +764 -0
- package/tests/rate-limiter.test.js +539 -0
- package/tests/retry.test.js +308 -0
- package/tests/security.test.js +411 -0
- package/tests/settings-modal.test.js +226 -0
- package/tests/theme-selector.test.js +57 -0
- package/tests/utils.js +242 -0
- package/tests/utils.test.js +317 -0
- package/tests/validate-plugin-cli.test.js +359 -0
- package/tests/validation.test.js +837 -0
- package/tests/web-server.test.js +646 -0
- package/tests/widget-arrange-mode.test.js +183 -0
- package/tests/widget-config-hot-reload.test.js +465 -0
- package/tests/widget-dependency.test.js +591 -0
- package/tests/widget-error-boundary.test.js +749 -0
- package/tests/widget-error-isolation.test.js +469 -0
- package/tests/widget-integration.test.js +1147 -0
- package/tests/widget-refresh-intervals.test.js +284 -0
- 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';
|