claw-dashboard 1.8.4 → 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 +5331 -512
- 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,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Watcher Module
|
|
3
|
+
* Watches configuration files for changes and triggers hot-reload
|
|
4
|
+
* Supports debouncing to avoid multiple reloads on rapid file changes
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { watch, watchFile, unwatchFile, existsSync, readdirSync, readFileSync } from 'fs';
|
|
8
|
+
import { EventEmitter } from 'events';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import logger from './logger.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Default watcher options
|
|
14
|
+
*/
|
|
15
|
+
export const DEFAULT_WATCHER_OPTIONS = {
|
|
16
|
+
debounceMs: 500, // Debounce interval for file changes
|
|
17
|
+
persistent: true, // Keep process running while watching
|
|
18
|
+
encoding: 'utf8', // File encoding
|
|
19
|
+
usePolling: false, // Use polling instead of native events (more reliable on some systems)
|
|
20
|
+
pollInterval: 1000, // Polling interval when usePolling is true
|
|
21
|
+
ignoreInitial: true, // Ignore the initial 'add' event
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* ConfigWatcher class
|
|
26
|
+
* Watches one or more config files for changes and emits reload events
|
|
27
|
+
*/
|
|
28
|
+
export class ConfigWatcher extends EventEmitter {
|
|
29
|
+
constructor(options = {}) {
|
|
30
|
+
super();
|
|
31
|
+
this.options = { ...DEFAULT_WATCHER_OPTIONS, ...options };
|
|
32
|
+
this.watchers = new Map(); // filepath -> FSWatcher
|
|
33
|
+
this.pollWatchers = new Map(); // filepath -> poll handle
|
|
34
|
+
this.lastModified = new Map(); // filepath -> timestamp
|
|
35
|
+
this.debounceTimers = new Map(); // filepath -> timer handle
|
|
36
|
+
this.watchedFiles = new Set(); // Set of watched file paths
|
|
37
|
+
this.isRunning = false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Start watching a config file
|
|
42
|
+
* @param {string} filePath - Path to the file to watch
|
|
43
|
+
* @param {Object} options - Optional override options
|
|
44
|
+
* @returns {boolean} True if successfully started watching
|
|
45
|
+
*/
|
|
46
|
+
watchFile(filePath, options = {}) {
|
|
47
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
48
|
+
logger.error('ConfigWatcher: Invalid file path provided');
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (this.watchers.has(filePath)) {
|
|
53
|
+
logger.debug(`ConfigWatcher: Already watching ${filePath}`);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const opts = { ...this.options, ...options };
|
|
58
|
+
|
|
59
|
+
// Check if file exists
|
|
60
|
+
if (!existsSync(filePath)) {
|
|
61
|
+
logger.warn(`ConfigWatcher: File not found: ${filePath}`);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
if (opts.usePolling) {
|
|
67
|
+
this._startPolling(filePath, opts);
|
|
68
|
+
} else {
|
|
69
|
+
this._startNativeWatch(filePath, opts);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.watchedFiles.add(filePath);
|
|
73
|
+
this.lastModified.set(filePath, Date.now());
|
|
74
|
+
this.isRunning = true;
|
|
75
|
+
|
|
76
|
+
logger.info(`ConfigWatcher: Started watching ${filePath}`);
|
|
77
|
+
return true;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
logger.error(`ConfigWatcher: Failed to watch ${filePath}: ${err.message}`);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Stop watching a config file
|
|
86
|
+
* @param {string} filePath - Path to stop watching
|
|
87
|
+
*/
|
|
88
|
+
unwatchFile(filePath) {
|
|
89
|
+
if (!this.watchers.has(filePath) && !this.pollWatchers.has(filePath)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Clear debounce timer if exists
|
|
94
|
+
const timer = this.debounceTimers.get(filePath);
|
|
95
|
+
if (timer) {
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
this.debounceTimers.delete(filePath);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Close native watcher
|
|
101
|
+
const watcher = this.watchers.get(filePath);
|
|
102
|
+
if (watcher) {
|
|
103
|
+
watcher.close();
|
|
104
|
+
this.watchers.delete(filePath);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Stop polling watcher
|
|
108
|
+
if (this.pollWatchers.has(filePath)) {
|
|
109
|
+
unwatchFile(filePath);
|
|
110
|
+
this.pollWatchers.delete(filePath);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.watchedFiles.delete(filePath);
|
|
114
|
+
this.lastModified.delete(filePath);
|
|
115
|
+
|
|
116
|
+
logger.info(`ConfigWatcher: Stopped watching ${filePath}`);
|
|
117
|
+
|
|
118
|
+
if (this.watchers.size === 0 && this.pollWatchers.size === 0) {
|
|
119
|
+
this.isRunning = false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Start watching multiple files
|
|
125
|
+
* @param {string[]} filePaths - Array of file paths to watch
|
|
126
|
+
* @returns {Object} Results with successful and failed paths
|
|
127
|
+
*/
|
|
128
|
+
watchFiles(filePaths) {
|
|
129
|
+
const results = { successful: [], failed: [] };
|
|
130
|
+
|
|
131
|
+
for (const filePath of filePaths) {
|
|
132
|
+
if (this.watchFile(filePath)) {
|
|
133
|
+
results.successful.push(filePath);
|
|
134
|
+
} else {
|
|
135
|
+
results.failed.push(filePath);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return results;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Stop watching all files
|
|
144
|
+
*/
|
|
145
|
+
unwatchAll() {
|
|
146
|
+
for (const filePath of this.watchedFiles) {
|
|
147
|
+
this.unwatchFile(filePath);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get list of watched files
|
|
153
|
+
* @returns {string[]} Array of watched file paths
|
|
154
|
+
*/
|
|
155
|
+
getWatchedFiles() {
|
|
156
|
+
return Array.from(this.watchedFiles);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Check if a file is being watched
|
|
161
|
+
* @param {string} filePath - Path to check
|
|
162
|
+
* @returns {boolean} True if being watched
|
|
163
|
+
*/
|
|
164
|
+
isWatching(filePath) {
|
|
165
|
+
return this.watchedFiles.has(filePath);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Start native file watcher (fs.watch)
|
|
170
|
+
* @private
|
|
171
|
+
*/
|
|
172
|
+
_startNativeWatch(filePath, opts) {
|
|
173
|
+
const watcher = watch(filePath, { persistent: opts.persistent, encoding: opts.encoding });
|
|
174
|
+
|
|
175
|
+
watcher.on('change', (eventType) => {
|
|
176
|
+
if (eventType === 'change') {
|
|
177
|
+
this._handleChange(filePath, opts);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
watcher.on('error', (err) => {
|
|
182
|
+
logger.error(`ConfigWatcher: Watcher error for ${filePath}: ${err.message}`);
|
|
183
|
+
this.emit('error', { filePath, error: err });
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
watcher.on('close', () => {
|
|
187
|
+
this.watchers.delete(filePath);
|
|
188
|
+
if (this.watchers.size === 0 && this.pollWatchers.size === 0) {
|
|
189
|
+
this.isRunning = false;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
this.watchers.set(filePath, watcher);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Start polling-based watcher (fs.watchFile)
|
|
198
|
+
* @private
|
|
199
|
+
*/
|
|
200
|
+
_startPolling(filePath, opts) {
|
|
201
|
+
watchFile(filePath, { persistent: opts.persistent, interval: opts.pollInterval }, (curr, prev) => {
|
|
202
|
+
if (curr.mtimeMs !== prev.mtimeMs) {
|
|
203
|
+
this._handleChange(filePath, opts);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
this.pollWatchers.set(filePath, true);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Handle file change with debouncing
|
|
212
|
+
* @private
|
|
213
|
+
*/
|
|
214
|
+
_handleChange(filePath, opts) {
|
|
215
|
+
const now = Date.now();
|
|
216
|
+
const last = this.lastModified.get(filePath) || 0;
|
|
217
|
+
|
|
218
|
+
// Update last modified time
|
|
219
|
+
this.lastModified.set(filePath, now);
|
|
220
|
+
|
|
221
|
+
// Clear existing debounce timer
|
|
222
|
+
const existingTimer = this.debounceTimers.get(filePath);
|
|
223
|
+
if (existingTimer) {
|
|
224
|
+
clearTimeout(existingTimer);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Set new debounce timer
|
|
228
|
+
const timer = setTimeout(() => {
|
|
229
|
+
this.debounceTimers.delete(filePath);
|
|
230
|
+
this._emitReload(filePath);
|
|
231
|
+
}, opts.debounceMs);
|
|
232
|
+
|
|
233
|
+
this.debounceTimers.set(filePath, timer);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Emit reload event for a file
|
|
238
|
+
* @private
|
|
239
|
+
*/
|
|
240
|
+
_emitReload(filePath) {
|
|
241
|
+
logger.info(`ConfigWatcher: File changed: ${filePath}`);
|
|
242
|
+
this.emit('reload', { filePath, timestamp: Date.now() });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Get watcher statistics
|
|
247
|
+
* @returns {Object} Stats object
|
|
248
|
+
*/
|
|
249
|
+
getStats() {
|
|
250
|
+
return {
|
|
251
|
+
isRunning: this.isRunning,
|
|
252
|
+
watchedFiles: this.watchedFiles.size,
|
|
253
|
+
nativeWatchers: this.watchers.size,
|
|
254
|
+
pollWatchers: this.pollWatchers.size,
|
|
255
|
+
pendingDebounces: this.debounceTimers.size,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Create a config preprocessor for widget config hot-reload
|
|
262
|
+
* @param {Object} options - Options for the watcher
|
|
263
|
+
* @returns {Object} ConfigWatcher instance
|
|
264
|
+
*/
|
|
265
|
+
export function createConfigWatcher(options = {}) {
|
|
266
|
+
return new ConfigWatcher(options);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Watch dashboard settings file and trigger callback on change
|
|
271
|
+
* @param {string} settingsPath - Path to settings file
|
|
272
|
+
* @param {Function} callback - Callback function(settings) to call on change
|
|
273
|
+
* @param {Object} options - Watcher options
|
|
274
|
+
* @returns {ConfigWatcher|null} Watcher instance or null on failure
|
|
275
|
+
*/
|
|
276
|
+
export function watchSettingsFile(settingsPath, callback, options = {}) {
|
|
277
|
+
if (!existsSync(settingsPath)) {
|
|
278
|
+
logger.warn(`ConfigWatcher: Settings file not found: ${settingsPath}`);
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const watcher = createConfigWatcher(options);
|
|
283
|
+
|
|
284
|
+
watcher.on('reload', async ({ filePath }) => {
|
|
285
|
+
try {
|
|
286
|
+
const content = readFileSync(filePath, 'utf8');
|
|
287
|
+
const settings = JSON.parse(content);
|
|
288
|
+
|
|
289
|
+
logger.info(`ConfigWatcher: Settings reloaded from ${filePath}`);
|
|
290
|
+
|
|
291
|
+
// Call the callback with new settings
|
|
292
|
+
if (typeof callback === 'function') {
|
|
293
|
+
await callback(settings, filePath);
|
|
294
|
+
}
|
|
295
|
+
} catch (err) {
|
|
296
|
+
logger.error(`ConfigWatcher: Failed to reload settings: ${err.message}`);
|
|
297
|
+
watcher.emit('error', { filePath, error: err });
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
watcher.on('error', ({ filePath, error }) => {
|
|
302
|
+
logger.error(`ConfigWatcher: Error for ${filePath}: ${error.message}`);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
if (!watcher.watchFile(settingsPath)) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return watcher;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Watch plugin config directory for changes
|
|
314
|
+
* @param {string} pluginsDir - Path to plugins directory
|
|
315
|
+
* @param {Function} callback - Callback function(pluginId, manifest) to call on change
|
|
316
|
+
* @param {Object} options - Watcher options
|
|
317
|
+
* @returns {ConfigWatcher|null} Watcher instance or null on failure
|
|
318
|
+
*/
|
|
319
|
+
export function watchPluginsDirectory(pluginsDir, callback, options = {}) {
|
|
320
|
+
if (!existsSync(pluginsDir)) {
|
|
321
|
+
logger.warn(`ConfigWatcher: Plugins directory not found: ${pluginsDir}`);
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const watcher = createConfigWatcher(options);
|
|
326
|
+
|
|
327
|
+
watcher.on('reload', async ({ filePath }) => {
|
|
328
|
+
try {
|
|
329
|
+
// Determine which plugin was modified
|
|
330
|
+
const relativePath = filePath.replace(pluginsDir, '');
|
|
331
|
+
const pluginId = relativePath.split('/')[1] || relativePath.split('\\')[1];
|
|
332
|
+
|
|
333
|
+
if (!pluginId) {
|
|
334
|
+
logger.debug(`ConfigWatcher: Could not determine plugin ID for ${filePath}`);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Read the manifest file
|
|
339
|
+
const manifestPath = join(pluginsDir, pluginId, 'plugin.json');
|
|
340
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
341
|
+
|
|
342
|
+
logger.info(`ConfigWatcher: Plugin ${pluginId} config changed`);
|
|
343
|
+
|
|
344
|
+
// Call the callback with plugin ID and manifest
|
|
345
|
+
if (typeof callback === 'function') {
|
|
346
|
+
await callback(pluginId, manifest, filePath);
|
|
347
|
+
}
|
|
348
|
+
} catch (err) {
|
|
349
|
+
logger.error(`ConfigWatcher: Failed to handle plugin change: ${err.message}`);
|
|
350
|
+
watcher.emit('error', { filePath, error: err });
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Watch all plugin.json files in the plugins directory
|
|
355
|
+
try {
|
|
356
|
+
const entries = readdirSync(pluginsDir, { withFileTypes: true });
|
|
357
|
+
let watchedCount = 0;
|
|
358
|
+
|
|
359
|
+
for (const entry of entries) {
|
|
360
|
+
if (!entry.isDirectory()) continue;
|
|
361
|
+
|
|
362
|
+
const pluginPath = join(pluginsDir, entry.name);
|
|
363
|
+
const manifestPath = join(pluginPath, 'plugin.json');
|
|
364
|
+
|
|
365
|
+
if (existsSync(manifestPath)) {
|
|
366
|
+
if (watcher.watchFile(manifestPath)) {
|
|
367
|
+
watchedCount++;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (watchedCount === 0) {
|
|
373
|
+
logger.info(`ConfigWatcher: No plugin configs found in ${pluginsDir}`);
|
|
374
|
+
} else {
|
|
375
|
+
logger.info(`ConfigWatcher: Watching ${watchedCount} plugin configs`);
|
|
376
|
+
}
|
|
377
|
+
} catch (err) {
|
|
378
|
+
logger.error(`ConfigWatcher: Failed to scan plugins directory: ${err.message}`);
|
|
379
|
+
watcher.unwatchAll();
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return watcher;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Singleton instance for simple use cases
|
|
387
|
+
let defaultWatcher = null;
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Get the default config watcher instance
|
|
391
|
+
* @param {Object} options - Options for creating the watcher if needed
|
|
392
|
+
* @returns {ConfigWatcher} Default watcher instance
|
|
393
|
+
*/
|
|
394
|
+
export function getConfigWatcher(options) {
|
|
395
|
+
if (!defaultWatcher) {
|
|
396
|
+
defaultWatcher = new ConfigWatcher(options);
|
|
397
|
+
}
|
|
398
|
+
return defaultWatcher;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export default ConfigWatcher;
|