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.
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 +5236 -566
  40. package/jest.config.js +11 -0
  41. package/man/clawdash.1 +276 -0
  42. package/package.json +54 -5
  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 +1941 -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 +1057 -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,1941 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Plugin scaffolding CLI for Claw Dashboard
5
+ * Provides `clawdash create-plugin <name>` functionality with multiple templates
6
+ */
7
+
8
+ import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { homedir } from 'os';
11
+ import readline from 'readline';
12
+
13
+ /**
14
+ * Create a readline interface for interactive prompts
15
+ * @returns {readline.Interface} Readline interface
16
+ */
17
+ function createReadlineInterface() {
18
+ return readline.createInterface({
19
+ input: process.stdin,
20
+ output: process.stdout,
21
+ });
22
+ }
23
+
24
+ /**
25
+ * Prompt for user input
26
+ * @param {string} question - Question to ask
27
+ * @returns {Promise<string>} User input
28
+ */
29
+ function prompt(question) {
30
+ return new Promise((resolve) => {
31
+ const rl = createReadlineInterface();
32
+ rl.question(question, (answer) => {
33
+ rl.close();
34
+ resolve(answer);
35
+ });
36
+ });
37
+ }
38
+
39
+ /**
40
+ * Prompt with default value
41
+ * @param {string} question - Question to ask
42
+ * @param {string} defaultValue - Default value
43
+ * @returns {Promise<string>} User input or default
44
+ */
45
+ async function promptWithDefault(question, defaultValue) {
46
+ const answer = await prompt(`${question} [${defaultValue}]: `);
47
+ return answer.trim() || defaultValue;
48
+ }
49
+
50
+ /**
51
+ * Prompt with multiple choice
52
+ * @param {string} question - Question to ask
53
+ * @param {string[]} choices - Available choices
54
+ * @param {number} defaultIndex - Default choice index
55
+ * @returns {Promise<string>} Selected choice
56
+ */
57
+ async function promptChoice(question, choices, defaultIndex = 0) {
58
+ const options = choices.map((c, i) => ` ${i + 1}. ${c}`).join('\n');
59
+
60
+ while (true) {
61
+ const answer = await prompt(`${question}\n${options}\nSelect (1-${choices.length}) [${defaultIndex + 1}]: `);
62
+
63
+ const input = answer.trim() || String(defaultIndex + 1);
64
+ const num = parseInt(input, 10);
65
+ if (!isNaN(num) && num >= 1 && num <= choices.length) {
66
+ return choices[num - 1];
67
+ }
68
+
69
+ console.log('Invalid selection. Please enter a number between 1 and ' + choices.length);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Run interactive mode to prompt for all options
75
+ * @returns {Promise<object>} Collected options
76
+ */
77
+ async function runInteractiveMode() {
78
+ console.log('');
79
+ console.log('╔══════════════════════════════════════════════════════════════╗');
80
+ console.log('║ Claw Dashboard - Create New Widget Plugin ║');
81
+ console.log('╚══════════════════════════════════════════════════════════════╝');
82
+ console.log('');
83
+
84
+ // Get plugin ID
85
+ const id = await prompt('Plugin ID (kebab-case, e.g., "my-widget"): ');
86
+ if (!id.trim()) {
87
+ console.log('Error: Plugin ID is required');
88
+ return null;
89
+ }
90
+
91
+ // Validate ID format
92
+ const idValidation = validatePluginId(id.trim());
93
+ if (!idValidation.valid) {
94
+ console.log('Error: ' + idValidation.error);
95
+ return null;
96
+ }
97
+
98
+ // Get display name
99
+ const defaultName = id.trim().split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
100
+ const name = await promptWithDefault('Display name', defaultName);
101
+
102
+ // Get template
103
+ const templates = listTemplates();
104
+ const templateNames = templates.map(t => t.name + ' (' + t.id + ')');
105
+ const selectedTemplate = await promptChoice('Select template:', templateNames, 0);
106
+ const template = templates[templateNames.indexOf(selectedTemplate)].id;
107
+
108
+ // Get author
109
+ const author = await promptWithDefault('Author name/email', '');
110
+
111
+ // Get category
112
+ const category = await promptChoice('Select category:', ['Custom', 'System', 'Monitoring', 'Example'], 0);
113
+
114
+ // Get description
115
+ const description = await promptWithDefault('Description', 'A custom widget for Claw Dashboard');
116
+
117
+ console.log('');
118
+ console.log('══════════════════════════════════════════════════════════════');
119
+ console.log('Summary:');
120
+ console.log(' ID: ' + id.trim());
121
+ console.log(' Name: ' + name);
122
+ console.log(' Template: ' + template);
123
+ console.log(' Category: ' + category.toLowerCase());
124
+ console.log(' Author: ' + (author || '(none)'));
125
+ console.log(' Description: ' + description);
126
+ console.log('══════════════════════════════════════════════════════════════');
127
+ console.log('');
128
+
129
+ const confirm = await promptChoice('Create plugin?', ['Yes', 'No'], 0);
130
+
131
+ if (confirm !== 'Yes') {
132
+ console.log('Cancelled.');
133
+ return null;
134
+ }
135
+
136
+ return {
137
+ id: id.trim(),
138
+ name,
139
+ template,
140
+ author,
141
+ category: category.toLowerCase(),
142
+ description,
143
+ };
144
+ }
145
+
146
+ /**
147
+ * Template definitions for different widget types
148
+ */
149
+ const TEMPLATES = {
150
+ basic: {
151
+ name: 'Basic Widget',
152
+ description: 'Simple widget with minimal setup - displays static or simple data',
153
+ manifest: (id, name, author, options = {}) => ({
154
+ id,
155
+ name: name || id.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
156
+ description: options.description || 'A custom widget plugin for Claw Dashboard',
157
+ version: '1.0.0',
158
+ author: author || '',
159
+ category: options.category || 'custom',
160
+ type: 'widget',
161
+ lazyLoad: true,
162
+ priority: 100,
163
+ config: {
164
+ message: 'Hello, World!',
165
+ showTimestamp: true,
166
+ },
167
+ __version: 1,
168
+ }),
169
+
170
+ widgetCode: (id, className) => `/**
171
+ * ${className} Widget Plugin
172
+ * Generated by clawdash create-plugin
173
+ */
174
+
175
+ import { BaseWidget } from 'claw-dashboard/widgets';
176
+
177
+ /**
178
+ * ${className} - A custom widget for Claw Dashboard
179
+ */
180
+ export default class ${className} extends BaseWidget {
181
+ constructor(options = {}) {
182
+ super(options);
183
+ this.name = options.name || '${className}';
184
+ this.description = options.description || 'A custom widget';
185
+ }
186
+
187
+ /**
188
+ * Initialize the widget
189
+ * Called once when the widget is first loaded
190
+ */
191
+ async init() {
192
+ this.log('info', '${className} widget initialized');
193
+ return true;
194
+ }
195
+
196
+ /**
197
+ * Create the widget UI
198
+ * @param {Object} screen - Blessed screen object
199
+ * @param {Object} theme - Theme colors
200
+ */
201
+ async create(screen, theme = {}) {
202
+ const C = theme.colors || {};
203
+ const blessed = await import('blessed');
204
+
205
+ // Create main container
206
+ this.box = blessed.default.box({
207
+ parent: screen,
208
+ width: '50%',
209
+ height: 10,
210
+ border: { type: 'line' },
211
+ label: ' ${className.toUpperCase()} ',
212
+ style: {
213
+ border: { fg: C.cyan || 'cyan' },
214
+ },
215
+ });
216
+
217
+ // Create content text
218
+ this.contentText = blessed.default.text({
219
+ parent: this.box,
220
+ top: 2,
221
+ left: 1,
222
+ content: 'Loading...',
223
+ style: { fg: C.white || 'white' },
224
+ });
225
+
226
+ this.loaded = true;
227
+ this.log('debug', '${className} widget UI created');
228
+
229
+ return this;
230
+ }
231
+
232
+ /**
233
+ * Get data for the widget
234
+ * Fetch and return data for rendering
235
+ */
236
+ async getData() {
237
+ const message = this.config.message || 'Hello, World!';
238
+ const showTimestamp = this.config.showTimestamp !== false;
239
+
240
+ return {
241
+ message,
242
+ timestamp: showTimestamp ? new Date().toISOString() : null,
243
+ };
244
+ }
245
+
246
+ /**
247
+ * Render the widget with data
248
+ * @param {Object} data - Data from getData()
249
+ */
250
+ render(data) {
251
+ if (!this.box || !data) return;
252
+
253
+ let content = data.message;
254
+ if (data.timestamp) {
255
+ content += '\\n[' + data.timestamp + ']';
256
+ }
257
+
258
+ this.contentText.setContent(content);
259
+ }
260
+
261
+ /**
262
+ * Destroy the widget
263
+ * Clean up resources
264
+ */
265
+ async destroy() {
266
+ if (this.box) {
267
+ this.box.destroy();
268
+ this.box = null;
269
+ }
270
+ this.loaded = false;
271
+ this.log('info', '${className} widget destroyed');
272
+ }
273
+ }
274
+
275
+ // Export named export for flexibility
276
+ export { ${className} };
277
+ `,
278
+ },
279
+
280
+ api: {
281
+ name: 'API Widget',
282
+ description: 'Widget with API fetching, error handling, and retry logic',
283
+ manifest: (id, name, author, options = {}) => ({
284
+ id,
285
+ name: name || id.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
286
+ description: options.description || 'A widget that fetches data from an external API',
287
+ version: '1.0.0',
288
+ author: author || '',
289
+ category: options.category || 'monitoring',
290
+ type: 'widget',
291
+ lazyLoad: true,
292
+ priority: 100,
293
+ config: {
294
+ apiUrl: '${API_URL:-https://api.github.com/zen}',
295
+ apiKey: '${API_KEY:-}',
296
+ refreshInterval: 60000,
297
+ timeout: 5000,
298
+ retries: 3,
299
+ },
300
+ __version: 1,
301
+ }),
302
+
303
+ widgetCode: (id, className) => `/**
304
+ * ${className} Widget Plugin
305
+ * API-powered widget with error handling and retry logic
306
+ */
307
+
308
+ import { BaseWidget } from 'claw-dashboard/widgets';
309
+
310
+ /**
311
+ * ${className} - API-powered widget for Claw Dashboard
312
+ */
313
+ export default class ${className} extends BaseWidget {
314
+ constructor(options = {}) {
315
+ super(options);
316
+ this.name = options.name || '${className}';
317
+ this.description = options.description || 'API-powered widget';
318
+
319
+ // Internal state
320
+ this.loading = false;
321
+ this.error = null;
322
+ this.lastFetch = null;
323
+ this.refreshTimer = null;
324
+ }
325
+
326
+ /**
327
+ * Initialize the widget
328
+ */
329
+ async init() {
330
+ this.log('info', '${className} widget initialized');
331
+ return true;
332
+ }
333
+
334
+ /**
335
+ * Create the widget UI
336
+ * @param {Object} screen - Blessed screen object
337
+ * @param {Object} theme - Theme colors
338
+ */
339
+ async create(screen, theme = {}) {
340
+ const C = theme.colors || {};
341
+ const blessed = await import('blessed');
342
+
343
+ this.screen = screen;
344
+ this.theme = theme;
345
+
346
+ // Main container
347
+ this.box = blessed.default.box({
348
+ parent: screen,
349
+ width: '50%',
350
+ height: 7,
351
+ border: { type: 'line' },
352
+ label: ' ${className.toUpperCase()} ',
353
+ style: { border: { fg: C.cyan || 'cyan' } },
354
+ });
355
+
356
+ // Status line
357
+ this.statusText = blessed.default.text({
358
+ parent: this.box,
359
+ top: 0,
360
+ left: 1,
361
+ content: 'Initializing...',
362
+ style: { fg: C.gray || 'gray' },
363
+ });
364
+
365
+ // Data content
366
+ this.contentText = blessed.default.text({
367
+ parent: this.box,
368
+ top: 1,
369
+ left: 1,
370
+ content: '',
371
+ style: { fg: C.white || 'white' },
372
+ wrap: true,
373
+ });
374
+
375
+ // Last updated
376
+ this.updatedText = blessed.default.text({
377
+ parent: this.box,
378
+ top: 3,
379
+ left: 1,
380
+ content: 'Never updated',
381
+ style: { fg: C.gray || 'gray' },
382
+ });
383
+
384
+ // Stats
385
+ this.statsText = blessed.default.text({
386
+ parent: this.box,
387
+ top: 4,
388
+ left: 1,
389
+ content: 'Requests: 0 | Errors: 0',
390
+ style: { fg: C.gray || 'gray' },
391
+ });
392
+
393
+ this.loaded = true;
394
+ this.log('debug', '${className} widget UI created');
395
+
396
+ // Start auto-refresh
397
+ const refreshInterval = this.config.refreshInterval || 60000;
398
+ if (refreshInterval > 0) {
399
+ this.startAutoRefresh(refreshInterval);
400
+ }
401
+
402
+ return this;
403
+ }
404
+
405
+ /**
406
+ * Fetch data from the configured API
407
+ */
408
+ async getData() {
409
+ const apiUrl = this.config.apiUrl || 'https://api.github.com/zen';
410
+ const timeout = this.config.timeout || 5000;
411
+ const maxRetries = this.config.retries || 3;
412
+
413
+ let lastError = null;
414
+
415
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
416
+ try {
417
+ this.loading = true;
418
+ this.error = null;
419
+ this.updateStatus('loading', 'Fetching (attempt ' + attempt + '/' + maxRetries + ')...');
420
+
421
+ const controller = new AbortController();
422
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
423
+
424
+ const response = await fetch(apiUrl, {
425
+ signal: controller.signal,
426
+ headers: {
427
+ 'User-Agent': 'Claw-Dashboard-Widget/1.0',
428
+ ...(this.config.apiKey && { 'Authorization': 'Bearer ' + this.config.apiKey }),
429
+ },
430
+ });
431
+
432
+ clearTimeout(timeoutId);
433
+
434
+ if (!response.ok) {
435
+ throw new Error('HTTP ' + response.status + ': ' + response.statusText);
436
+ }
437
+
438
+ // Handle different response types
439
+ const contentType = response.headers.get('content-type') || '';
440
+ let data;
441
+
442
+ if (contentType.includes('application/json')) {
443
+ data = await response.json();
444
+ } else {
445
+ data = await response.text();
446
+ }
447
+
448
+ this.loading = false;
449
+ this.lastFetch = new Date();
450
+
451
+ return {
452
+ success: true,
453
+ data,
454
+ timestamp: this.lastFetch.toISOString(),
455
+ apiUrl,
456
+ };
457
+ } catch (err) {
458
+ lastError = err;
459
+ this.log('warn', 'API fetch attempt ' + attempt + ' failed: ' + err.message);
460
+
461
+ if (err.name === 'AbortError') {
462
+ break;
463
+ }
464
+
465
+ // Exponential backoff
466
+ if (attempt < maxRetries) {
467
+ await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
468
+ }
469
+ }
470
+ }
471
+
472
+ // All retries failed
473
+ this.loading = false;
474
+ this.error = lastError;
475
+
476
+ return {
477
+ success: false,
478
+ error: lastError?.message || 'Unknown error',
479
+ timestamp: new Date().toISOString(),
480
+ apiUrl,
481
+ };
482
+ }
483
+
484
+ /**
485
+ * Render the widget with fetched data
486
+ */
487
+ render(result) {
488
+ if (!this.box) return;
489
+
490
+ if (result.success) {
491
+ this.updateStatus('success', 'Connected');
492
+
493
+ // Format the data for display
494
+ let content;
495
+ if (typeof result.data === 'object') {
496
+ content = JSON.stringify(result.data, null, 0).slice(0, 100);
497
+ } else {
498
+ content = String(result.data).slice(0, 100);
499
+ }
500
+
501
+ this.contentText.setContent(content);
502
+ this.contentText.style.fg = this.theme?.colors?.white || 'white';
503
+ } else {
504
+ this.updateStatus('error', 'Error: ' + result.error);
505
+ this.contentText.setContent('Unable to fetch data');
506
+ this.contentText.style.fg = this.theme?.colors?.red || 'red';
507
+ }
508
+
509
+ this.updatedText.setContent('Last: ' + (result.timestamp || 'Never'));
510
+
511
+ const stats = this.getStats();
512
+ this.statsText.setContent('Requests: ' + stats.requests + ' | Errors: ' + stats.errors);
513
+ }
514
+
515
+ /**
516
+ * Update the status indicator
517
+ */
518
+ updateStatus(status, message) {
519
+ const colors = {
520
+ loading: this.theme?.colors?.yellow || 'yellow',
521
+ success: this.theme?.colors?.green || 'green',
522
+ error: this.theme?.colors?.red || 'red',
523
+ };
524
+
525
+ this.statusText.setContent(message);
526
+ this.statusText.style.fg = colors[status] || 'white';
527
+ }
528
+
529
+ /**
530
+ * Track request statistics
531
+ */
532
+ getStats() {
533
+ if (!this._stats) {
534
+ this._stats = { requests: 0, errors: 0 };
535
+ }
536
+ return this._stats;
537
+ }
538
+
539
+ /**
540
+ * Start auto-refresh timer
541
+ */
542
+ startAutoRefresh(intervalMs) {
543
+ this.stopAutoRefresh();
544
+ this.refreshTimer = setInterval(() => {
545
+ if (!this.loading) {
546
+ this.getData()
547
+ .then(data => this.render(data))
548
+ .catch(err => this.log('error', 'Auto-refresh failed: ' + err.message));
549
+ }
550
+ }, intervalMs);
551
+
552
+ this.log('debug', 'Auto-refresh started (' + intervalMs + 'ms interval)');
553
+ }
554
+
555
+ /**
556
+ * Stop auto-refresh timer
557
+ */
558
+ stopAutoRefresh() {
559
+ if (this.refreshTimer) {
560
+ clearInterval(this.refreshTimer);
561
+ this.refreshTimer = null;
562
+ this.log('debug', 'Auto-refresh stopped');
563
+ }
564
+ }
565
+
566
+ /**
567
+ * Destroy the widget
568
+ */
569
+ async destroy() {
570
+ this.stopAutoRefresh();
571
+
572
+ if (this.box) {
573
+ this.box.destroy();
574
+ this.box = null;
575
+ }
576
+
577
+ this.loaded = false;
578
+ this.log('info', '${className} widget destroyed');
579
+ }
580
+ }
581
+
582
+ export { ${className} };
583
+ `,
584
+ },
585
+
586
+ chart: {
587
+ name: 'Chart Widget',
588
+ description: 'Widget with real-time line chart visualization using blessed-contrib',
589
+ manifest: (id, name, author, options = {}) => ({
590
+ id,
591
+ name: name || id.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
592
+ description: options.description || 'A widget that displays real-time data with charts',
593
+ version: '1.0.0',
594
+ author: author || '',
595
+ category: options.category || 'monitoring',
596
+ type: 'widget',
597
+ lazyLoad: true,
598
+ priority: 100,
599
+ config: {
600
+ metricType: 'cpu',
601
+ maxDataPoints: 30,
602
+ refreshInterval: 2000,
603
+ showLegend: true,
604
+ },
605
+ __version: 1,
606
+ }),
607
+
608
+ widgetCode: (id, className) => `/**
609
+ * ${className} Widget Plugin
610
+ * Chart widget with real-time data visualization using blessed-contrib
611
+ */
612
+
613
+ import { BaseWidget } from 'claw-dashboard/widgets';
614
+
615
+ /**
616
+ * ${className} - Chart widget for Claw Dashboard
617
+ */
618
+ export default class ${className} extends BaseWidget {
619
+ constructor(options = {}) {
620
+ super(options);
621
+ this.name = options.name || '${className}';
622
+ this.description = options.description || 'Chart widget with real-time data';
623
+
624
+ // Chart configuration
625
+ this.metricType = this.config.metricType || 'cpu';
626
+ this.maxDataPoints = this.config.maxDataPoints || 30;
627
+ this.refreshInterval = this.config.refreshInterval || 2000;
628
+ this.showLegend = this.config.showLegend !== false;
629
+
630
+ // Data storage for time series
631
+ this.dataHistory = { labels: [], values: [] };
632
+
633
+ // Widget state
634
+ this.chart = null;
635
+ this.refreshTimer = null;
636
+ }
637
+
638
+ /**
639
+ * Initialize the widget
640
+ */
641
+ async init() {
642
+ this.log('info', '${className} widget initialized');
643
+ return true;
644
+ }
645
+
646
+ /**
647
+ * Create the widget UI with blessed-contrib line chart
648
+ * @param {Object} screen - Blessed screen object
649
+ * @param {Object} theme - Theme colors
650
+ */
651
+ async create(screen, theme = {}) {
652
+ const C = theme.colors || {};
653
+ const blessed = await import('blessed');
654
+ const contrib = await import('blessed-contrib');
655
+
656
+ this.screen = screen;
657
+ this.theme = theme;
658
+
659
+ // Create main container box
660
+ this.box = blessed.default.box({
661
+ parent: screen,
662
+ width: '70%',
663
+ height: 17,
664
+ border: { type: 'line' },
665
+ label: ' ${className.toUpperCase()} ',
666
+ style: {
667
+ border: { fg: C.cyan || 'cyan' },
668
+ },
669
+ });
670
+
671
+ // Create the line chart using blessed-contrib
672
+ this.chart = contrib.default.line({
673
+ parent: this.box,
674
+ top: 1,
675
+ left: 1,
676
+ width: '95%',
677
+ height: 14,
678
+ style: {
679
+ line: C.green || 'green',
680
+ text: C.white || 'white',
681
+ baseline: C.gray || 'gray',
682
+ },
683
+ xLabelPadding: 3,
684
+ xPadding: 5,
685
+ numYLabels: 5,
686
+ showNthLabel: Math.ceil(this.maxDataPoints / 6),
687
+ showLegend: this.showLegend,
688
+ legend: { width: 12 },
689
+ minY: 0,
690
+ maxY: 100,
691
+ wholeNumbersOnly: true,
692
+ });
693
+
694
+ // Add info text
695
+ this.infoText = blessed.default.text({
696
+ parent: this.box,
697
+ bottom: 0,
698
+ left: 1,
699
+ content: 'Initializing...',
700
+ style: { fg: C.gray || 'gray' },
701
+ });
702
+
703
+ this.loaded = true;
704
+ this.log('debug', '${className} widget UI created');
705
+
706
+ // Start auto-refresh
707
+ this.startAutoRefresh();
708
+
709
+ return this;
710
+ }
711
+
712
+ /**
713
+ * Generate sample data - customize this for your data source
714
+ */
715
+ async getData() {
716
+ const now = new Date();
717
+ const timeLabel = now.getHours().toString().padStart(2, '0') + ':' +
718
+ now.getMinutes().toString().padStart(2, '0') + ':' +
719
+ now.getSeconds().toString().padStart(2, '0');
720
+
721
+ // Generate sample data - replace with actual data fetching
722
+ const baseValue = 30;
723
+ const variance = Math.random() * 40;
724
+ const value = Math.min(100, Math.max(0, Math.floor(baseValue + variance)));
725
+
726
+ // Store in history
727
+ this.dataHistory.labels.push(timeLabel);
728
+ this.dataHistory.values.push(value);
729
+
730
+ // Trim to max data points
731
+ if (this.dataHistory.labels.length > this.maxDataPoints) {
732
+ this.dataHistory.labels.shift();
733
+ this.dataHistory.values.shift();
734
+ }
735
+
736
+ return {
737
+ currentValue: value,
738
+ timestamp: now.toISOString(),
739
+ labels: [...this.dataHistory.labels],
740
+ values: [...this.dataHistory.values],
741
+ dataPoints: this.dataHistory.values.length,
742
+ };
743
+ }
744
+
745
+ /**
746
+ * Render the chart with data
747
+ */
748
+ render(data) {
749
+ if (!this.chart || !data) return;
750
+
751
+ // Prepare chart data
752
+ const chartData = {
753
+ title: '${className}',
754
+ x: data.labels,
755
+ y: data.values,
756
+ style: { line: 'green' },
757
+ };
758
+
759
+ // Update the chart
760
+ this.chart.setData([chartData]);
761
+
762
+ // Update info text
763
+ const avg = data.values.length > 0
764
+ ? Math.floor(data.values.reduce((a, b) => a + b, 0) / data.values.length)
765
+ : 0;
766
+ const current = data.currentValue;
767
+ const max = data.values.length > 0 ? Math.max(...data.values) : 0;
768
+
769
+ this.infoText.setContent(
770
+ 'Current: ' + current + ' | Avg: ' + avg + ' | Peak: ' + max +
771
+ ' | Points: ' + data.dataPoints + '/' + this.maxDataPoints
772
+ );
773
+ }
774
+
775
+ /**
776
+ * Start auto-refresh timer
777
+ */
778
+ startAutoRefresh() {
779
+ this.stopAutoRefresh();
780
+
781
+ if (this.refreshInterval > 0) {
782
+ this.refreshTimer = setInterval(async () => {
783
+ try {
784
+ const data = await this.getData();
785
+ this.render(data);
786
+ } catch (err) {
787
+ this.log('error', 'Auto-refresh failed: ' + err.message);
788
+ }
789
+ }, this.refreshInterval);
790
+
791
+ this.log('debug', 'Auto-refresh started (' + this.refreshInterval + 'ms)');
792
+ }
793
+ }
794
+
795
+ /**
796
+ * Stop auto-refresh timer
797
+ */
798
+ stopAutoRefresh() {
799
+ if (this.refreshTimer) {
800
+ clearInterval(this.refreshTimer);
801
+ this.refreshTimer = null;
802
+ this.log('debug', 'Auto-refresh stopped');
803
+ }
804
+ }
805
+
806
+ /**
807
+ * Destroy the widget
808
+ */
809
+ async destroy() {
810
+ this.stopAutoRefresh();
811
+
812
+ // Clear data history
813
+ this.dataHistory = { labels: [], values: [] };
814
+
815
+ if (this.chart) {
816
+ this.chart = null;
817
+ }
818
+
819
+ if (this.box) {
820
+ this.box.destroy();
821
+ this.box = null;
822
+ }
823
+
824
+ this.loaded = false;
825
+ this.log('info', '${className} widget destroyed');
826
+ }
827
+ }
828
+
829
+ export { ${className} };
830
+ `,
831
+ },
832
+
833
+ table: {
834
+ name: 'Table Widget',
835
+ description: 'Widget that displays data in a sortable table format',
836
+ manifest: (id, name, author, options = {}) => ({
837
+ id,
838
+ name: name || id.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
839
+ description: options.description || 'A widget that displays tabular data',
840
+ version: '1.0.0',
841
+ author: author || '',
842
+ category: options.category || 'monitoring',
843
+ type: 'widget',
844
+ lazyLoad: true,
845
+ priority: 100,
846
+ config: {
847
+ columns: ['Name', 'Status', 'Value'],
848
+ refreshInterval: 5000,
849
+ maxRows: 10,
850
+ },
851
+ __version: 1,
852
+ }),
853
+
854
+ widgetCode: (id, className) => `/**
855
+ * ${className} Widget Plugin
856
+ * Table widget for displaying tabular data
857
+ */
858
+
859
+ import { BaseWidget } from 'claw-dashboard/widgets';
860
+
861
+ /**
862
+ * ${className} - Table widget for Claw Dashboard
863
+ */
864
+ export default class ${className} extends BaseWidget {
865
+ constructor(options = {}) {
866
+ super(options);
867
+ this.name = options.name || '${className}';
868
+ this.description = options.description || 'Table widget';
869
+
870
+ // Table configuration
871
+ this.columns = this.config.columns || ['Name', 'Status', 'Value'];
872
+ this.refreshInterval = this.config.refreshInterval || 5000;
873
+ this.maxRows = this.config.maxRows || 10;
874
+
875
+ // Widget state
876
+ this.table = null;
877
+ this.refreshTimer = null;
878
+ this.sortColumn = 0;
879
+ this.sortAsc = true;
880
+ this.data = [];
881
+ }
882
+
883
+ /**
884
+ * Initialize the widget
885
+ */
886
+ async init() {
887
+ this.log('info', '${className} widget initialized');
888
+ return true;
889
+ }
890
+
891
+ /**
892
+ * Create the widget UI
893
+ * @param {Object} screen - Blessed screen object
894
+ * @param {Object} theme - Theme colors
895
+ */
896
+ async create(screen, theme = {}) {
897
+ const C = theme.colors || {};
898
+ const blessed = await import('blessed');
899
+
900
+ this.screen = screen;
901
+ this.theme = theme;
902
+
903
+ // Main container
904
+ this.box = blessed.default.box({
905
+ parent: screen,
906
+ width: '60%',
907
+ height: 15,
908
+ border: { type: 'line' },
909
+ label: ' ${className.toUpperCase()} ',
910
+ style: {
911
+ border: { fg: C.cyan || 'cyan' },
912
+ },
913
+ });
914
+
915
+ // Create table
916
+ this.table = blessed.default.table({
917
+ parent: this.box,
918
+ top: 1,
919
+ left: 1,
920
+ width: '98%',
921
+ height: '90%',
922
+ border: { type: 'none' },
923
+ style: {
924
+ header: { fg: C.cyan || 'cyan', bold: true },
925
+ cell: { fg: C.white || 'white' },
926
+ },
927
+ columns: this.columns,
928
+ rows: [],
929
+ });
930
+
931
+ this.loaded = true;
932
+ this.log('debug', '${className} widget UI created');
933
+
934
+ // Start auto-refresh
935
+ this.startAutoRefresh();
936
+
937
+ return this;
938
+ }
939
+
940
+ /**
941
+ * Generate sample data - customize this for your data source
942
+ */
943
+ async getData() {
944
+ // Sample data - replace with actual data fetching
945
+ const sampleData = [
946
+ ['Server 1', 'Online', Math.floor(Math.random() * 100) + '%'],
947
+ ['Server 2', 'Online', Math.floor(Math.random() * 100) + '%'],
948
+ ['Server 3', 'Warning', Math.floor(Math.random() * 100) + '%'],
949
+ ['Server 4', 'Online', Math.floor(Math.random() * 100) + '%'],
950
+ ['Server 5', 'Offline', '0%'],
951
+ ];
952
+
953
+ // Sort data
954
+ const sorted = [...sampleData].sort((a, b) => {
955
+ const aVal = a[this.sortColumn];
956
+ const bVal = b[this.sortColumn];
957
+ const cmp = aVal.localeCompare(bVal);
958
+ return this.sortAsc ? cmp : -cmp;
959
+ });
960
+
961
+ this.data = sorted.slice(0, this.maxRows);
962
+
963
+ return {
964
+ rows: this.data,
965
+ timestamp: new Date().toISOString(),
966
+ };
967
+ }
968
+
969
+ /**
970
+ * Render the table with data
971
+ */
972
+ render(result) {
973
+ if (!this.table || !result) return;
974
+
975
+ this.table.setData({
976
+ headers: this.columns,
977
+ rows: result.rows,
978
+ });
979
+ }
980
+
981
+ /**
982
+ * Start auto-refresh timer
983
+ */
984
+ startAutoRefresh() {
985
+ this.stopAutoRefresh();
986
+
987
+ if (this.refreshInterval > 0) {
988
+ this.refreshTimer = setInterval(async () => {
989
+ try {
990
+ const data = await this.getData();
991
+ this.render(data);
992
+ } catch (err) {
993
+ this.log('error', 'Auto-refresh failed: ' + err.message);
994
+ }
995
+ }, this.refreshInterval);
996
+
997
+ this.log('debug', 'Auto-refresh started (' + this.refreshInterval + 'ms)');
998
+ }
999
+ }
1000
+
1001
+ /**
1002
+ * Stop auto-refresh timer
1003
+ */
1004
+ stopAutoRefresh() {
1005
+ if (this.refreshTimer) {
1006
+ clearInterval(this.refreshTimer);
1007
+ this.refreshTimer = null;
1008
+ this.log('debug', 'Auto-refresh stopped');
1009
+ }
1010
+ }
1011
+
1012
+ /**
1013
+ * Destroy the widget
1014
+ */
1015
+ async destroy() {
1016
+ this.stopAutoRefresh();
1017
+
1018
+ if (this.table) {
1019
+ this.table.destroy();
1020
+ this.table = null;
1021
+ }
1022
+
1023
+ if (this.box) {
1024
+ this.box.destroy();
1025
+ this.box = null;
1026
+ }
1027
+
1028
+ this.loaded = false;
1029
+ this.log('info', '${className} widget destroyed');
1030
+ }
1031
+ }
1032
+
1033
+ export { ${className} };
1034
+ `,
1035
+ },
1036
+
1037
+ gauge: {
1038
+ name: 'Gauge Widget',
1039
+ description: 'Widget that displays a circular or linear gauge for single metrics',
1040
+ manifest: (id, name, author, options = {}) => ({
1041
+ id,
1042
+ name: name || id.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
1043
+ description: options.description || 'A widget that displays metrics with a gauge',
1044
+ version: '1.0.0',
1045
+ author: author || '',
1046
+ category: options.category || 'monitoring',
1047
+ type: 'widget',
1048
+ lazyLoad: true,
1049
+ priority: 100,
1050
+ config: {
1051
+ gaugeType: 'circle',
1052
+ minValue: 0,
1053
+ maxValue: 100,
1054
+ refreshInterval: 2000,
1055
+ unit: '%',
1056
+ },
1057
+ __version: 1,
1058
+ }),
1059
+
1060
+ widgetCode: (id, className) => `/**
1061
+ * ${className} Widget Plugin
1062
+ * Gauge widget for displaying single metrics
1063
+ */
1064
+
1065
+ import { BaseWidget } from 'claw-dashboard/widgets';
1066
+
1067
+ /**
1068
+ * ${className} - Gauge widget for Claw Dashboard
1069
+ */
1070
+ export default class ${className} extends BaseWidget {
1071
+ constructor(options = {}) {
1072
+ super(options);
1073
+ this.name = options.name || '${className}';
1074
+ this.description = options.description || 'Gauge widget';
1075
+
1076
+ // Gauge configuration
1077
+ this.gaugeType = this.config.gaugeType || 'circle';
1078
+ this.minValue = this.config.minValue || 0;
1079
+ this.maxValue = this.config.maxValue || 100;
1080
+ this.refreshInterval = this.config.refreshInterval || 2000;
1081
+ this.unit = this.config.unit || '%';
1082
+
1083
+ // Widget state
1084
+ this.gauge = null;
1085
+ this.refreshTimer = null;
1086
+ this.currentValue = 0;
1087
+ }
1088
+
1089
+ /**
1090
+ * Initialize the widget
1091
+ */
1092
+ async init() {
1093
+ this.log('info', '${className} widget initialized');
1094
+ return true;
1095
+ }
1096
+
1097
+ /**
1098
+ * Create the widget UI
1099
+ * @param {Object} screen - Blessed screen object
1100
+ * @param {Object} theme - Theme colors
1101
+ */
1102
+ async create(screen, theme = {}) {
1103
+ const C = theme.colors || {};
1104
+ const blessed = await import('blessed');
1105
+ const contrib = await import('blessed-contrib');
1106
+
1107
+ this.screen = screen;
1108
+ this.theme = theme;
1109
+
1110
+ // Main container
1111
+ this.box = blessed.default.box({
1112
+ parent: screen,
1113
+ width: '30%',
1114
+ height: 10,
1115
+ border: { type: 'line' },
1116
+ label: ' ${className.toUpperCase()} ',
1117
+ style: {
1118
+ border: { fg: C.cyan || 'cyan' },
1119
+ },
1120
+ });
1121
+
1122
+ // Create gauge based on type
1123
+ if (this.gaugeType === 'circle') {
1124
+ this.gauge = contrib.default.gauge({
1125
+ parent: this.box,
1126
+ top: 1,
1127
+ left: 'center',
1128
+ width: '90%',
1129
+ height: 6,
1130
+ style: {
1131
+ label: { fg: C.white || 'white' },
1132
+ value: { fg: C.green || 'green' },
1133
+ },
1134
+ label: this.name,
1135
+ });
1136
+ } else {
1137
+ // Linear gauge
1138
+ this.gauge = contrib.default.gauge({
1139
+ parent: this.box,
1140
+ top: 2,
1141
+ left: 1,
1142
+ width: '96%',
1143
+ height: 4,
1144
+ style: {
1145
+ label: { fg: C.white || 'white' },
1146
+ value: { fg: C.green || 'green' },
1147
+ },
1148
+ label: this.name,
1149
+ });
1150
+ }
1151
+
1152
+ // Value display
1153
+ this.valueText = blessed.default.text({
1154
+ parent: this.box,
1155
+ bottom: 0,
1156
+ left: 'center',
1157
+ content: '0' + this.unit,
1158
+ style: { fg: C.white || 'white', bold: true },
1159
+ });
1160
+
1161
+ this.loaded = true;
1162
+ this.log('debug', '${className} widget UI created');
1163
+
1164
+ // Start auto-refresh
1165
+ this.startAutoRefresh();
1166
+
1167
+ return this;
1168
+ }
1169
+
1170
+ /**
1171
+ * Generate sample data - customize this for your data source
1172
+ */
1173
+ async getData() {
1174
+ // Sample data - replace with actual data fetching
1175
+ const value = Math.floor(Math.random() * (this.maxValue - this.minValue) + this.minValue);
1176
+ this.currentValue = value;
1177
+
1178
+ return {
1179
+ value: value,
1180
+ percentage: ((value - this.minValue) / (this.maxValue - this.minValue)) * 100,
1181
+ timestamp: new Date().toISOString(),
1182
+ };
1183
+ }
1184
+
1185
+ /**
1186
+ * Render the gauge with data
1187
+ */
1188
+ render(result) {
1189
+ if (!this.gauge || !result) return;
1190
+
1191
+ // Set gauge percentage (0-100)
1192
+ this.gauge.setData(result.percentage);
1193
+
1194
+ // Update value text
1195
+ this.valueText.setContent(result.value + this.unit);
1196
+
1197
+ // Color based on value
1198
+ let color = 'green';
1199
+ if (result.percentage > 80) {
1200
+ color = 'red';
1201
+ } else if (result.percentage > 60) {
1202
+ color = 'yellow';
1203
+ }
1204
+
1205
+ this.valueText.style.fg = this.theme?.colors?.[color] || color;
1206
+ }
1207
+
1208
+ /**
1209
+ * Start auto-refresh timer
1210
+ */
1211
+ startAutoRefresh() {
1212
+ this.stopAutoRefresh();
1213
+
1214
+ if (this.refreshInterval > 0) {
1215
+ this.refreshTimer = setInterval(async () => {
1216
+ try {
1217
+ const data = await this.getData();
1218
+ this.render(data);
1219
+ } catch (err) {
1220
+ this.log('error', 'Auto-refresh failed: ' + err.message);
1221
+ }
1222
+ }, this.refreshInterval);
1223
+
1224
+ this.log('debug', 'Auto-refresh started (' + this.refreshInterval + 'ms)');
1225
+ }
1226
+ }
1227
+
1228
+ /**
1229
+ * Stop auto-refresh timer
1230
+ */
1231
+ stopAutoRefresh() {
1232
+ if (this.refreshTimer) {
1233
+ clearInterval(this.refreshTimer);
1234
+ this.refreshTimer = null;
1235
+ this.log('debug', 'Auto-refresh stopped');
1236
+ }
1237
+ }
1238
+
1239
+ /**
1240
+ * Destroy the widget
1241
+ */
1242
+ async destroy() {
1243
+ this.stopAutoRefresh();
1244
+
1245
+ if (this.gauge) {
1246
+ this.gauge.destroy();
1247
+ this.gauge = null;
1248
+ }
1249
+
1250
+ if (this.box) {
1251
+ this.box.destroy();
1252
+ this.box = null;
1253
+ }
1254
+
1255
+ this.loaded = false;
1256
+ this.log('info', '${className} widget destroyed');
1257
+ }
1258
+ }
1259
+
1260
+ export { ${className} };
1261
+ `,
1262
+ },
1263
+
1264
+ logViewer: {
1265
+ name: 'Log Viewer Widget',
1266
+ description: 'Widget that displays scrolling log entries with filtering',
1267
+ manifest: (id, name, author, options = {}) => ({
1268
+ id,
1269
+ name: name || id.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
1270
+ description: options.description || 'A widget that displays scrolling log entries',
1271
+ version: '1.0.0',
1272
+ author: author || '',
1273
+ category: options.category || 'monitoring',
1274
+ type: 'widget',
1275
+ lazyLoad: true,
1276
+ priority: 100,
1277
+ config: {
1278
+ maxLines: 50,
1279
+ showTimestamp: true,
1280
+ filterLevels: ['info', 'warn', 'error'],
1281
+ refreshInterval: 1000,
1282
+ },
1283
+ __version: 1,
1284
+ }),
1285
+
1286
+ widgetCode: (id, className) => `/**
1287
+ * ${className} Widget Plugin
1288
+ * Log viewer widget for displaying scrolling log entries
1289
+ */
1290
+
1291
+ import { BaseWidget } from 'claw-dashboard/widgets';
1292
+
1293
+ /**
1294
+ * ${className} - Log viewer widget for Claw Dashboard
1295
+ */
1296
+ export default class ${className} extends BaseWidget {
1297
+ constructor(options = {}) {
1298
+ super(options);
1299
+ this.name = options.name || '${className}';
1300
+ this.description = options.description || 'Log viewer widget';
1301
+
1302
+ // Log viewer configuration
1303
+ this.maxLines = this.config.maxLines || 50;
1304
+ this.showTimestamp = this.config.showTimestamp !== false;
1305
+ this.filterLevels = this.config.filterLevels || ['info', 'warn', 'error'];
1306
+ this.refreshInterval = this.config.refreshInterval || 1000;
1307
+
1308
+ // Widget state
1309
+ this.logBox = null;
1310
+ this.refreshTimer = null;
1311
+ this.logEntries = [];
1312
+ }
1313
+
1314
+ /**
1315
+ * Initialize the widget
1316
+ */
1317
+ async init() {
1318
+ this.log('info', '${className} widget initialized');
1319
+
1320
+ // Add initial log entries
1321
+ this.addLogEntry('info', 'Log viewer initialized');
1322
+ this.addLogEntry('info', 'Waiting for log data...');
1323
+
1324
+ return true;
1325
+ }
1326
+
1327
+ /**
1328
+ * Add a log entry
1329
+ * @param {string} level - Log level (info, warn, error)
1330
+ * @param {string} message - Log message
1331
+ */
1332
+ addLogEntry(level, message) {
1333
+ const entry = {
1334
+ level,
1335
+ message,
1336
+ timestamp: new Date(),
1337
+ };
1338
+
1339
+ this.logEntries.push(entry);
1340
+
1341
+ // Trim to max lines
1342
+ if (this.logEntries.length > this.maxLines) {
1343
+ this.logEntries.shift();
1344
+ }
1345
+ }
1346
+
1347
+ /**
1348
+ * Create the widget UI
1349
+ * @param {Object} screen - Blessed screen object
1350
+ * @param {Object} theme - Theme colors
1351
+ */
1352
+ async create(screen, theme = {}) {
1353
+ const C = theme.colors || {};
1354
+ const blessed = await import('blessed');
1355
+
1356
+ this.screen = screen;
1357
+ this.theme = theme;
1358
+
1359
+ // Main container
1360
+ this.box = blessed.default.box({
1361
+ parent: screen,
1362
+ width: '70%',
1363
+ height: 15,
1364
+ border: { type: 'line' },
1365
+ label: ' ${className.toUpperCase()} ',
1366
+ style: {
1367
+ border: { fg: C.cyan || 'cyan' },
1368
+ },
1369
+ });
1370
+
1371
+ // Log entries box with scrolling
1372
+ this.logBox = blessed.default.log({
1373
+ parent: this.box,
1374
+ top: 1,
1375
+ left: 1,
1376
+ width: '98%',
1377
+ height: '90%',
1378
+ scrollable: true,
1379
+ scrollbar: {
1380
+ style: {
1381
+ bg: C.gray || 'gray',
1382
+ },
1383
+ },
1384
+ style: {
1385
+ fg: C.white || 'white',
1386
+ bg: C.black || 'black',
1387
+ },
1388
+ });
1389
+
1390
+ this.loaded = true;
1391
+ this.log('debug', '${className} widget UI created');
1392
+
1393
+ // Initial render
1394
+ this.renderLogs();
1395
+
1396
+ // Start auto-refresh
1397
+ this.startAutoRefresh();
1398
+
1399
+ return this;
1400
+ }
1401
+
1402
+ /**
1403
+ * Get filtered log entries
1404
+ */
1405
+ async getData() {
1406
+ // Sample log generation - replace with actual log fetching
1407
+ const levels = ['info', 'info', 'info', 'warn', 'error'];
1408
+ const messages = [
1409
+ 'Request processed successfully',
1410
+ 'Connection established',
1411
+ 'Data synchronized',
1412
+ 'High memory usage detected',
1413
+ 'Failed to connect to service',
1414
+ ];
1415
+
1416
+ // Randomly add new log entry
1417
+ if (Math.random() > 0.7) {
1418
+ const level = levels[Math.floor(Math.random() * levels.length)];
1419
+ const message = messages[Math.floor(Math.random() * messages.length)];
1420
+ this.addLogEntry(level, message);
1421
+ }
1422
+
1423
+ // Filter by level
1424
+ const filtered = this.logEntries.filter(entry =>
1425
+ this.filterLevels.includes(entry.level)
1426
+ );
1427
+
1428
+ return {
1429
+ entries: filtered,
1430
+ timestamp: new Date().toISOString(),
1431
+ };
1432
+ }
1433
+
1434
+ /**
1435
+ * Render the log entries
1436
+ */
1437
+ renderLogs() {
1438
+ if (!this.logBox) return;
1439
+
1440
+ this.logBox.setContent('');
1441
+
1442
+ for (const entry of this.logEntries) {
1443
+ if (!this.filterLevels.includes(entry.level)) continue;
1444
+
1445
+ let line = '';
1446
+
1447
+ if (this.showTimestamp) {
1448
+ const time = entry.timestamp.toLocaleTimeString();
1449
+ line += '[' + time + '] ';
1450
+ }
1451
+
1452
+ const levelStr = entry.level.toUpperCase().padEnd(5);
1453
+ line += '[' + levelStr + '] ' + entry.message;
1454
+
1455
+ // Set color based on level
1456
+ const colorMap = {
1457
+ info: this.theme?.colors?.white || 'white',
1458
+ warn: this.theme?.colors?.yellow || 'yellow',
1459
+ error: this.theme?.colors?.red || 'red',
1460
+ };
1461
+
1462
+ this.logBox.add(line, colorMap[entry.level] || 'white');
1463
+ }
1464
+
1465
+ // Scroll to bottom
1466
+ this.logBox.setScrollPerc(100);
1467
+ }
1468
+
1469
+ /**
1470
+ * Render the widget with data
1471
+ */
1472
+ render(result) {
1473
+ if (!result) return;
1474
+ this.renderLogs();
1475
+ }
1476
+
1477
+ /**
1478
+ * Start auto-refresh timer
1479
+ */
1480
+ startAutoRefresh() {
1481
+ this.stopAutoRefresh();
1482
+
1483
+ if (this.refreshInterval > 0) {
1484
+ this.refreshTimer = setInterval(async () => {
1485
+ try {
1486
+ const data = await this.getData();
1487
+ this.render(data);
1488
+ } catch (err) {
1489
+ this.log('error', 'Auto-refresh failed: ' + err.message);
1490
+ }
1491
+ }, this.refreshInterval);
1492
+
1493
+ this.log('debug', 'Auto-refresh started (' + this.refreshInterval + 'ms)');
1494
+ }
1495
+ }
1496
+
1497
+ /**
1498
+ * Stop auto-refresh timer
1499
+ */
1500
+ stopAutoRefresh() {
1501
+ if (this.refreshTimer) {
1502
+ clearInterval(this.refreshTimer);
1503
+ this.refreshTimer = null;
1504
+ this.log('debug', 'Auto-refresh stopped');
1505
+ }
1506
+ }
1507
+
1508
+ /**
1509
+ * Destroy the widget
1510
+ */
1511
+ async destroy() {
1512
+ this.stopAutoRefresh();
1513
+
1514
+ if (this.logBox) {
1515
+ this.logBox.destroy();
1516
+ this.logBox = null;
1517
+ }
1518
+
1519
+ if (this.box) {
1520
+ this.box.destroy();
1521
+ this.box = null;
1522
+ }
1523
+
1524
+ this.logEntries = [];
1525
+ this.loaded = false;
1526
+ this.log('info', '${className} widget destroyed');
1527
+ }
1528
+ }
1529
+
1530
+ export { ${className} };
1531
+ `,
1532
+ },
1533
+ };
1534
+
1535
+ /**
1536
+ * Converts a plugin ID to a valid JavaScript class name
1537
+ * @param {string} id - Plugin ID (e.g., "my-custom-widget")
1538
+ * @returns {string} Class name (e.g., "MyCustomWidget")
1539
+ */
1540
+ function toClassName(id) {
1541
+ return id
1542
+ .split(/[-_]/)
1543
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
1544
+ .join('');
1545
+ }
1546
+
1547
+ /**
1548
+ * Validates a plugin ID
1549
+ * @param {string} id - Plugin ID
1550
+ * @returns {object} Validation result
1551
+ */
1552
+ function validatePluginId(id) {
1553
+ if (!id || typeof id !== 'string') {
1554
+ return { valid: false, error: 'Plugin ID must be a non-empty string' };
1555
+ }
1556
+
1557
+ if (id.length < 1 || id.length > 64) {
1558
+ return { valid: false, error: 'Plugin ID must be between 1 and 64 characters' };
1559
+ }
1560
+
1561
+ // Check for valid characters
1562
+ const validPattern = /^[a-zA-Z0-9]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$/;
1563
+ if (!validPattern.test(id)) {
1564
+ return {
1565
+ valid: false,
1566
+ error: 'Plugin ID must contain only alphanumeric characters, hyphens, and underscores, and cannot start or end with a hyphen/underscore',
1567
+ };
1568
+ }
1569
+
1570
+ // Check for reserved names
1571
+ const reservedNames = ['claw', 'dashboard', 'admin', 'system', 'test'];
1572
+ if (reservedNames.includes(id.toLowerCase())) {
1573
+ return { valid: false, error: `'${id}' is a reserved name` };
1574
+ }
1575
+
1576
+ return { valid: true };
1577
+ }
1578
+
1579
+ /**
1580
+ * Generate README content
1581
+ * @param {string} id - Plugin ID
1582
+ * @param {string} name - Plugin name
1583
+ * @param {string} templateType - Template type
1584
+ * @returns {string} README content
1585
+ */
1586
+ function generateReadme(id, name, templateType) {
1587
+ const template = TEMPLATES[templateType] || TEMPLATES.basic;
1588
+ return `# ${name}
1589
+
1590
+ ${template.description} for Claw Dashboard.
1591
+
1592
+ ## Installation
1593
+
1594
+ 1. Copy this directory to your Claw Dashboard plugins folder:
1595
+ \`\`\`bash
1596
+ cp -r ${id} ~/.openclaw/plugins/
1597
+ \`\`\`
1598
+
1599
+ 2. Restart Claw Dashboard or reload plugins
1600
+
1601
+ ## Configuration
1602
+
1603
+ Edit \`plugin.json\` to customize the widget:
1604
+
1605
+ \`\`\`json
1606
+ {
1607
+ "config": {
1608
+ // See plugin.json for available options
1609
+ }
1610
+ }
1611
+ \`\`\`
1612
+
1613
+ ## Development
1614
+
1615
+ ### File Structure
1616
+
1617
+ \`\`\`
1618
+ ${id}/
1619
+ ├── plugin.json # Plugin manifest
1620
+ ├── index.js # Widget code
1621
+ └── README.md # This file
1622
+ \`\`\`
1623
+
1624
+ ### Testing
1625
+
1626
+ Run your widget in Claw Dashboard:
1627
+
1628
+ \`\`\`bash
1629
+ clawdash --debug
1630
+ \`\`\`
1631
+
1632
+ ## API Reference
1633
+
1634
+ See [Claw Dashboard Plugin Documentation](https://github.com/spleck/claw-dashboard/blob/main/docs/PLUGINS.md) for full API reference.
1635
+
1636
+ ## License
1637
+
1638
+ MIT
1639
+ `;
1640
+ }
1641
+
1642
+ /**
1643
+ * List available templates
1644
+ * @returns {object[]} Array of template info
1645
+ */
1646
+ export function listTemplates() {
1647
+ return Object.entries(TEMPLATES).map(([key, template]) => ({
1648
+ id: key,
1649
+ name: template.name,
1650
+ description: template.description,
1651
+ }));
1652
+ }
1653
+
1654
+ /**
1655
+ * Creates a plugin scaffold
1656
+ * @param {string} id - Plugin ID
1657
+ * @param {object} options - Options
1658
+ * @returns {object} Result
1659
+ */
1660
+ export async function createPlugin(id, options = {}) {
1661
+ const {
1662
+ name,
1663
+ author,
1664
+ outputDir,
1665
+ template = 'basic',
1666
+ dryRun = false,
1667
+ force = false,
1668
+ } = options;
1669
+
1670
+ // Validate plugin ID
1671
+ const validation = validatePluginId(id);
1672
+ if (!validation.valid) {
1673
+ return {
1674
+ success: false,
1675
+ error: validation.error,
1676
+ code: 'INVALID_ID',
1677
+ };
1678
+ }
1679
+
1680
+ // Validate template
1681
+ const selectedTemplate = TEMPLATES[template];
1682
+ if (!selectedTemplate) {
1683
+ return {
1684
+ success: false,
1685
+ error: `Unknown template: ${template}. Available: ${Object.keys(TEMPLATES).join(', ')}`,
1686
+ code: 'INVALID_TEMPLATE',
1687
+ };
1688
+ }
1689
+
1690
+ // Determine output directory
1691
+ const pluginsDir = outputDir || join(homedir(), '.openclaw', 'plugins');
1692
+ const pluginDir = join(pluginsDir, id);
1693
+
1694
+ // Check if plugin already exists
1695
+ if (existsSync(pluginDir) && !force) {
1696
+ return {
1697
+ success: false,
1698
+ error: `Plugin directory already exists: ${pluginDir}`,
1699
+ code: 'ALREADY_EXISTS',
1700
+ path: pluginDir,
1701
+ };
1702
+ }
1703
+
1704
+ // Generate class name from ID
1705
+ const className = toClassName(id);
1706
+
1707
+ // Generate files
1708
+ const files = {
1709
+ 'plugin.json': JSON.stringify(
1710
+ selectedTemplate.manifest(id, name, author, options),
1711
+ null,
1712
+ 2
1713
+ ),
1714
+ 'index.js': selectedTemplate.widgetCode(id, className),
1715
+ 'README.md': generateReadme(id, name || id, template),
1716
+ };
1717
+
1718
+ // In dry-run mode, just return what would be created
1719
+ if (dryRun) {
1720
+ return {
1721
+ success: true,
1722
+ dryRun: true,
1723
+ path: pluginDir,
1724
+ files: Object.keys(files),
1725
+ template,
1726
+ };
1727
+ }
1728
+
1729
+ // Create directory
1730
+ try {
1731
+ mkdirSync(pluginDir, { recursive: true });
1732
+ } catch (err) {
1733
+ return {
1734
+ success: false,
1735
+ error: `Failed to create directory: ${err.message}`,
1736
+ code: 'MKDIR_ERROR',
1737
+ };
1738
+ }
1739
+
1740
+ // Write files
1741
+ const createdFiles = [];
1742
+ for (const [filename, content] of Object.entries(files)) {
1743
+ const filePath = join(pluginDir, filename);
1744
+ try {
1745
+ writeFileSync(filePath, content);
1746
+ createdFiles.push(filename);
1747
+ } catch (err) {
1748
+ return {
1749
+ success: false,
1750
+ error: `Failed to write ${filename}: ${err.message}`,
1751
+ code: 'WRITE_ERROR',
1752
+ path: filePath,
1753
+ };
1754
+ }
1755
+ }
1756
+
1757
+ return {
1758
+ success: true,
1759
+ path: pluginDir,
1760
+ files: createdFiles,
1761
+ id,
1762
+ className,
1763
+ template,
1764
+ };
1765
+ }
1766
+
1767
+ /**
1768
+ * Main CLI handler
1769
+ * @param {string[]} args - CLI arguments
1770
+ * @returns {number} Exit code
1771
+ */
1772
+ export async function runScaffoldCli(args) {
1773
+ const command = args[0];
1774
+
1775
+ if (!command || command === '--help' || command === '-h') {
1776
+ console.log(`
1777
+ Plugin Scaffolding CLI for Claw Dashboard
1778
+
1779
+ Usage: clawdash create-plugin <id> [options]
1780
+
1781
+ Arguments:
1782
+ id Plugin ID (kebab-case, e.g., "my-custom-widget")
1783
+
1784
+ Options:
1785
+ -t, --template Template to use (basic, api, chart, table, gauge, logViewer)
1786
+ Default: basic
1787
+ -n, --name Display name for the widget
1788
+ -a, --author Author name or email
1789
+ -c, --category Widget category (system, monitoring, custom, example)
1790
+ Default: custom
1791
+ --desc Widget description
1792
+ -o, --output Output directory (default: ~/.openclaw/plugins/)
1793
+ -f, --force Overwrite existing plugin
1794
+ --dry-run Show what would be created without creating it
1795
+ --list-templates Show available templates
1796
+ -i, --interactive Start interactive mode (prompts for all options)
1797
+ -h, --help Show this help message
1798
+
1799
+ Examples:
1800
+ clawdash create-plugin my-widget
1801
+ clawdash create-plugin api-status --template api --author "John Doe"
1802
+ clawdash create-plugin metrics --template chart --category monitoring
1803
+ clawdash create-plugin my-widget --interactive
1804
+ clawdash create-plugin --list-templates
1805
+ `);
1806
+ return 0;
1807
+ }
1808
+
1809
+ if (command === '--version' || command === '-v') {
1810
+ console.log('clawdash-create-plugin 1.1.0');
1811
+ return 0;
1812
+ }
1813
+
1814
+ if (command === '--list-templates' || command === 'list-templates') {
1815
+ console.log('Available Templates:');
1816
+ console.log('');
1817
+ const templates = listTemplates();
1818
+ templates.forEach(t => {
1819
+ console.log(` ${t.id.padEnd(12)} ${t.name}`);
1820
+ console.log(` ${t.description}`);
1821
+ console.log('');
1822
+ });
1823
+ return 0;
1824
+ }
1825
+
1826
+ // Parse arguments
1827
+ const options = {
1828
+ name: undefined,
1829
+ author: undefined,
1830
+ category: 'custom',
1831
+ description: undefined,
1832
+ template: 'basic',
1833
+ outputDir: undefined,
1834
+ force: false,
1835
+ dryRun: false,
1836
+ };
1837
+
1838
+ let pluginId = null;
1839
+
1840
+ for (let i = 0; i < args.length; i++) {
1841
+ const arg = args[i];
1842
+
1843
+ if (!arg.startsWith('-') && !pluginId) {
1844
+ pluginId = arg;
1845
+ continue;
1846
+ }
1847
+
1848
+ switch (arg) {
1849
+ case '-t':
1850
+ case '--template':
1851
+ options.template = args[++i];
1852
+ break;
1853
+ case '-n':
1854
+ case '--name':
1855
+ options.name = args[++i];
1856
+ break;
1857
+ case '-a':
1858
+ case '--author':
1859
+ options.author = args[++i];
1860
+ break;
1861
+ case '-c':
1862
+ case '--category':
1863
+ options.category = args[++i];
1864
+ break;
1865
+ case '--desc':
1866
+ options.description = args[++i];
1867
+ break;
1868
+ case '-o':
1869
+ case '--output':
1870
+ options.outputDir = args[++i];
1871
+ break;
1872
+ case '-f':
1873
+ case '--force':
1874
+ options.force = true;
1875
+ break;
1876
+ case '--dry-run':
1877
+ options.dryRun = true;
1878
+ break;
1879
+ case '-i':
1880
+ case '--interactive':
1881
+ options.interactive = true;
1882
+ break;
1883
+ case '-h':
1884
+ case '--help':
1885
+ // Already handled above
1886
+ break;
1887
+ }
1888
+ }
1889
+
1890
+ // Handle interactive mode
1891
+ if (options.interactive) {
1892
+ const interactiveOptions = await runInteractiveMode();
1893
+ if (!interactiveOptions) {
1894
+ return 0; // User cancelled
1895
+ }
1896
+ // Merge interactive options with any passed CLI options
1897
+ Object.assign(options, interactiveOptions);
1898
+ pluginId = interactiveOptions.id;
1899
+ }
1900
+
1901
+ if (!pluginId) {
1902
+ console.error('Error: Plugin ID is required');
1903
+ console.error('Run with --help for usage information');
1904
+ return 1;
1905
+ }
1906
+
1907
+ const result = await createPlugin(pluginId, options);
1908
+
1909
+ if (!result.success) {
1910
+ console.error(`Error: ${result.error}`);
1911
+ return 1;
1912
+ }
1913
+
1914
+ if (result.dryRun) {
1915
+ console.log('Dry run - would create:');
1916
+ console.log(` Directory: ${result.path}`);
1917
+ console.log(` Template: ${result.template}`);
1918
+ console.log(' Files:');
1919
+ result.files.forEach(f => console.log(` - ${f}`));
1920
+ } else {
1921
+ console.log(`✓ Created plugin: ${pluginId}`);
1922
+ console.log(` Path: ${result.path}`);
1923
+ console.log(` Template: ${result.template}`);
1924
+ console.log(` Files: ${result.files.join(', ')}`);
1925
+ console.log('');
1926
+ console.log('Next steps:');
1927
+ console.log(` 1. Edit ${result.path}/index.js to implement your widget`);
1928
+ console.log(` 2. Update ${result.path}/plugin.json with your configuration`);
1929
+ console.log(' 3. Run clawdash to see your widget in action');
1930
+ }
1931
+
1932
+ return 0;
1933
+ }
1934
+
1935
+ // Run if called directly
1936
+ if (import.meta.url === `file://${process.argv[1]}`) {
1937
+ (async () => {
1938
+ const exitCode = await runScaffoldCli(process.argv.slice(2));
1939
+ process.exit(exitCode);
1940
+ })();
1941
+ }