claw-dashboard 1.9.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/.c8rc.json +38 -0
  2. package/.dockerignore +68 -0
  3. package/.github/dependabot.yml +45 -0
  4. package/.github/pull_request_template.md +39 -0
  5. package/.github/workflows/ci.yml +147 -0
  6. package/.github/workflows/release.yml +40 -0
  7. package/.github/workflows/security.yml +84 -0
  8. package/.husky/pre-commit +1 -0
  9. package/.planning/codebase/ARCHITECTURE.md +206 -0
  10. package/.planning/codebase/INTEGRATIONS.md +150 -0
  11. package/.planning/codebase/STACK.md +122 -0
  12. package/.planning/codebase/STRUCTURE.md +201 -0
  13. package/CHANGELOG.md +293 -0
  14. package/CONTRIBUTING.md +378 -0
  15. package/Dockerfile +54 -0
  16. package/FEATURES.md +195 -21
  17. package/README.md +83 -6
  18. package/TODO.md +28 -0
  19. package/build-cjs.js +127 -0
  20. package/cjs-shim.js +13 -0
  21. package/dist/clawdash +1729 -0
  22. package/dist/clawdash.meta.json +2236 -0
  23. package/dist/widgets.cjs +6511 -0
  24. package/docker-compose.yml +67 -0
  25. package/docs/API.md +1050 -0
  26. package/docs/PLUGINS.md +1504 -0
  27. package/esbuild.config.js +158 -0
  28. package/eslint.config.js +56 -0
  29. package/examples/plugins/README.md +122 -0
  30. package/examples/plugins/api-status/index.js +294 -0
  31. package/examples/plugins/api-status/plugin.json +19 -0
  32. package/examples/plugins/hello-world/index.js +104 -0
  33. package/examples/plugins/hello-world/plugin.json +15 -0
  34. package/examples/plugins/system-metrics-chart/index.js +339 -0
  35. package/examples/plugins/system-metrics-chart/plugin.json +18 -0
  36. package/examples/plugins/weather-widget/index.js +136 -0
  37. package/examples/plugins/weather-widget/plugin.json +19 -0
  38. package/index.cjs +23005 -0
  39. package/index.js +5285 -566
  40. package/jest.config.js +11 -0
  41. package/man/clawdash.1 +276 -0
  42. package/package.json +54 -6
  43. package/schemas/plugin-manifest.json +128 -0
  44. package/scripts/release.js +595 -0
  45. package/src/alerts.js +693 -0
  46. package/src/auto-save.js +584 -0
  47. package/src/cache.js +390 -0
  48. package/src/checksum.js +146 -0
  49. package/src/cli/args.js +110 -0
  50. package/src/cli/export-schedule.js +423 -0
  51. package/src/cli/export-snapshot.js +138 -0
  52. package/src/cli/help.js +69 -0
  53. package/src/cli/import-snapshot.js +230 -0
  54. package/src/cli/index.js +14 -0
  55. package/src/cli/list-templates.js +69 -0
  56. package/src/cli/validate-config.js +76 -0
  57. package/src/cli/validate-plugin.js +187 -0
  58. package/src/cli/version.js +15 -0
  59. package/src/config-validator.js +586 -0
  60. package/src/config-watcher.js +401 -0
  61. package/src/config.js +684 -0
  62. package/src/container-detector.js +499 -0
  63. package/src/database.js +734 -0
  64. package/src/differential-render.js +327 -0
  65. package/src/errors.js +169 -0
  66. package/src/export-scheduler.js +581 -0
  67. package/src/gateway-manager.js +685 -0
  68. package/src/hints.js +371 -0
  69. package/src/loading-states.js +509 -0
  70. package/src/logger.js +185 -0
  71. package/src/memory-pressure.js +467 -0
  72. package/src/performance-monitor.js +374 -0
  73. package/src/plugin-errors.js +586 -0
  74. package/src/plugin-manifest-validator.js +219 -0
  75. package/src/plugin-reload.js +481 -0
  76. package/src/plugin-scaffold.js +1934 -0
  77. package/src/plugin-validator.js +284 -0
  78. package/src/retry.js +218 -0
  79. package/src/security.js +860 -0
  80. package/src/snapshot.js +372 -0
  81. package/src/splash.js +115 -0
  82. package/src/theme-selector.js +411 -0
  83. package/src/themes.js +874 -0
  84. package/src/transitions.js +534 -0
  85. package/src/types.d.ts +174 -0
  86. package/src/validation.js +926 -0
  87. package/src/web-server.js +885 -0
  88. package/src/widgets/builtin-widgets.js +1056 -0
  89. package/src/widgets/config-processor.js +401 -0
  90. package/src/widgets/dependency-resolver.js +596 -0
  91. package/src/widgets/index.js +79 -0
  92. package/src/widgets/plugin-api.js +845 -0
  93. package/src/widgets/widget-error-boundary.js +773 -0
  94. package/src/widgets/widget-error-isolation.js +551 -0
  95. package/src/widgets/widget-loader.js +1437 -0
  96. package/src/workers/system-worker.js +130 -0
  97. package/src/workers/worker-pool.js +633 -0
  98. package/tests/alerts.test.js +437 -0
  99. package/tests/auto-save.test.js +529 -0
  100. package/tests/cache.test.js +317 -0
  101. package/tests/cli.test.js +351 -0
  102. package/tests/command-palette.test.js +320 -0
  103. package/tests/config-processor.test.js +452 -0
  104. package/tests/config-validator.test.js +710 -0
  105. package/tests/config-watcher.test.js +594 -0
  106. package/tests/config.test.js +755 -0
  107. package/tests/database.test.js +438 -0
  108. package/tests/errors.test.js +189 -0
  109. package/tests/example-plugins.test.js +624 -0
  110. package/tests/integration.test.js +1321 -0
  111. package/tests/loading-states.test.js +300 -0
  112. package/tests/manifest-validation-on-load.test.js +671 -0
  113. package/tests/performance-monitor.test.js +190 -0
  114. package/tests/plugin-api-rate-limit.test.js +302 -0
  115. package/tests/plugin-errors.test.js +311 -0
  116. package/tests/plugin-lifecycle-e2e.test.js +1036 -0
  117. package/tests/plugin-reload.test.js +764 -0
  118. package/tests/rate-limiter.test.js +539 -0
  119. package/tests/retry.test.js +308 -0
  120. package/tests/security.test.js +411 -0
  121. package/tests/settings-modal.test.js +226 -0
  122. package/tests/theme-selector.test.js +57 -0
  123. package/tests/utils.js +242 -0
  124. package/tests/utils.test.js +317 -0
  125. package/tests/validate-plugin-cli.test.js +359 -0
  126. package/tests/validation.test.js +837 -0
  127. package/tests/web-server.test.js +646 -0
  128. package/tests/widget-arrange-mode.test.js +183 -0
  129. package/tests/widget-config-hot-reload.test.js +465 -0
  130. package/tests/widget-dependency.test.js +591 -0
  131. package/tests/widget-error-boundary.test.js +749 -0
  132. package/tests/widget-error-isolation.test.js +469 -0
  133. package/tests/widget-integration.test.js +1147 -0
  134. package/tests/widget-refresh-intervals.test.js +284 -0
  135. package/tests/worker-pool.test.js +522 -0
@@ -0,0 +1,1934 @@
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 ASCII art',
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 ASCII art
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 ASCII 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
+
655
+ this.screen = screen;
656
+ this.theme = theme;
657
+
658
+ // Create main container box
659
+ this.box = blessed.default.box({
660
+ parent: screen,
661
+ width: '70%',
662
+ height: 17,
663
+ border: { type: 'line' },
664
+ label: ' ${className.toUpperCase()} ',
665
+ style: {
666
+ border: { fg: C.cyan || 'cyan' },
667
+ },
668
+ });
669
+
670
+ // Create ASCII chart area using text element
671
+ this.chartArea = blessed.default.text({
672
+ parent: this.box,
673
+ top: 1,
674
+ left: 1,
675
+ width: '95%',
676
+ height: 13,
677
+ tags: true,
678
+ style: { fg: C.green || 'green' },
679
+ });
680
+
681
+ // Add info text
682
+ this.infoText = blessed.default.text({
683
+ parent: this.box,
684
+ bottom: 0,
685
+ left: 1,
686
+ content: 'Initializing...',
687
+ style: { fg: C.gray || 'gray' },
688
+ });
689
+
690
+ this.loaded = true;
691
+ this.log('debug', '${className} widget UI created');
692
+
693
+ // Start auto-refresh
694
+ this.startAutoRefresh();
695
+
696
+ return this;
697
+ }
698
+
699
+ /**
700
+ * Generate ASCII chart from data
701
+ */
702
+ _renderAsciiChart(values, width = 60, height = 10) {
703
+ if (!values || values.length === 0) return 'No data';
704
+
705
+ const min = Math.min(...values);
706
+ const max = Math.max(...values);
707
+ const range = max - min || 1;
708
+
709
+ let chart = '';
710
+ for (let row = height - 1; row >= 0; row--) {
711
+ const threshold = min + (range * row / height);
712
+ let line = '';
713
+ for (let i = 0; i < Math.min(values.length, width); i++) {
714
+ const val = values[i];
715
+ line += val >= threshold ? '█' : '░';
716
+ }
717
+ chart += line + '\n';
718
+ }
719
+ return chart;
720
+ }
721
+
722
+ /**
723
+ * Generate sample data - customize this for your data source
724
+ */
725
+ async getData() {
726
+ const now = new Date();
727
+ const timeLabel = now.getHours().toString().padStart(2, '0') + ':' +
728
+ now.getMinutes().toString().padStart(2, '0') + ':' +
729
+ now.getSeconds().toString().padStart(2, '0');
730
+
731
+ // Generate sample data - replace with actual data fetching
732
+ const baseValue = 30;
733
+ const variance = Math.random() * 40;
734
+ const value = Math.min(100, Math.max(0, Math.floor(baseValue + variance)));
735
+
736
+ // Store in history
737
+ this.dataHistory.labels.push(timeLabel);
738
+ this.dataHistory.values.push(value);
739
+
740
+ // Trim to max data points
741
+ if (this.dataHistory.labels.length > this.maxDataPoints) {
742
+ this.dataHistory.labels.shift();
743
+ this.dataHistory.values.shift();
744
+ }
745
+
746
+ return {
747
+ currentValue: value,
748
+ timestamp: now.toISOString(),
749
+ labels: [...this.dataHistory.labels],
750
+ values: [...this.dataHistory.values],
751
+ dataPoints: this.dataHistory.values.length,
752
+ };
753
+ }
754
+
755
+ /**
756
+ * Render the chart with data
757
+ */
758
+ render(data) {
759
+ if (!this.chart || !data) return;
760
+
761
+ // Generate ASCII chart
762
+ const asciiChart = this._renderAsciiChart(data.values);
763
+ this.chartArea.setContent(asciiChart);
764
+
765
+ // Update info text
766
+ const avg = data.values.length > 0
767
+ ? Math.floor(data.values.reduce((a, b) => a + b, 0) / data.values.length)
768
+ : 0;
769
+ const current = data.currentValue;
770
+ const max = data.values.length > 0 ? Math.max(...data.values) : 0;
771
+
772
+ this.infoText.setContent(
773
+ 'Current: ' + current + ' | Avg: ' + avg + ' | Peak: ' + max +
774
+ ' | Points: ' + data.dataPoints + '/' + this.maxDataPoints
775
+ );
776
+ }
777
+
778
+ /**
779
+ * Start auto-refresh timer
780
+ */
781
+ startAutoRefresh() {
782
+ this.stopAutoRefresh();
783
+
784
+ if (this.refreshInterval > 0) {
785
+ this.refreshTimer = setInterval(async () => {
786
+ try {
787
+ const data = await this.getData();
788
+ this.render(data);
789
+ } catch (err) {
790
+ this.log('error', 'Auto-refresh failed: ' + err.message);
791
+ }
792
+ }, this.refreshInterval);
793
+
794
+ this.log('debug', 'Auto-refresh started (' + this.refreshInterval + 'ms)');
795
+ }
796
+ }
797
+
798
+ /**
799
+ * Stop auto-refresh timer
800
+ */
801
+ stopAutoRefresh() {
802
+ if (this.refreshTimer) {
803
+ clearInterval(this.refreshTimer);
804
+ this.refreshTimer = null;
805
+ this.log('debug', 'Auto-refresh stopped');
806
+ }
807
+ }
808
+
809
+ /**
810
+ * Destroy the widget
811
+ */
812
+ async destroy() {
813
+ this.stopAutoRefresh();
814
+
815
+ // Clear data history
816
+ this.dataHistory = { labels: [], values: [] };
817
+
818
+ if (this.chart) {
819
+ this.chart = null;
820
+ }
821
+
822
+ if (this.box) {
823
+ this.box.destroy();
824
+ this.box = null;
825
+ }
826
+
827
+ this.loaded = false;
828
+ this.log('info', '${className} widget destroyed');
829
+ }
830
+ }
831
+
832
+ export { ${className} };
833
+ `,
834
+ },
835
+
836
+ table: {
837
+ name: 'Table Widget',
838
+ description: 'Widget that displays data in a sortable table format',
839
+ manifest: (id, name, author, options = {}) => ({
840
+ id,
841
+ name: name || id.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
842
+ description: options.description || 'A widget that displays tabular data',
843
+ version: '1.0.0',
844
+ author: author || '',
845
+ category: options.category || 'monitoring',
846
+ type: 'widget',
847
+ lazyLoad: true,
848
+ priority: 100,
849
+ config: {
850
+ columns: ['Name', 'Status', 'Value'],
851
+ refreshInterval: 5000,
852
+ maxRows: 10,
853
+ },
854
+ __version: 1,
855
+ }),
856
+
857
+ widgetCode: (id, className) => `/**
858
+ * ${className} Widget Plugin
859
+ * Table widget for displaying tabular data
860
+ */
861
+
862
+ import { BaseWidget } from 'claw-dashboard/widgets';
863
+
864
+ /**
865
+ * ${className} - Table widget for Claw Dashboard
866
+ */
867
+ export default class ${className} extends BaseWidget {
868
+ constructor(options = {}) {
869
+ super(options);
870
+ this.name = options.name || '${className}';
871
+ this.description = options.description || 'Table widget';
872
+
873
+ // Table configuration
874
+ this.columns = this.config.columns || ['Name', 'Status', 'Value'];
875
+ this.refreshInterval = this.config.refreshInterval || 5000;
876
+ this.maxRows = this.config.maxRows || 10;
877
+
878
+ // Widget state
879
+ this.table = null;
880
+ this.refreshTimer = null;
881
+ this.sortColumn = 0;
882
+ this.sortAsc = true;
883
+ this.data = [];
884
+ }
885
+
886
+ /**
887
+ * Initialize the widget
888
+ */
889
+ async init() {
890
+ this.log('info', '${className} widget initialized');
891
+ return true;
892
+ }
893
+
894
+ /**
895
+ * Create the widget UI
896
+ * @param {Object} screen - Blessed screen object
897
+ * @param {Object} theme - Theme colors
898
+ */
899
+ async create(screen, theme = {}) {
900
+ const C = theme.colors || {};
901
+ const blessed = await import('blessed');
902
+
903
+ this.screen = screen;
904
+ this.theme = theme;
905
+
906
+ // Main container
907
+ this.box = blessed.default.box({
908
+ parent: screen,
909
+ width: '60%',
910
+ height: 15,
911
+ border: { type: 'line' },
912
+ label: ' ${className.toUpperCase()} ',
913
+ style: {
914
+ border: { fg: C.cyan || 'cyan' },
915
+ },
916
+ });
917
+
918
+ // Create table
919
+ this.table = blessed.default.table({
920
+ parent: this.box,
921
+ top: 1,
922
+ left: 1,
923
+ width: '98%',
924
+ height: '90%',
925
+ border: { type: 'none' },
926
+ style: {
927
+ header: { fg: C.cyan || 'cyan', bold: true },
928
+ cell: { fg: C.white || 'white' },
929
+ },
930
+ columns: this.columns,
931
+ rows: [],
932
+ });
933
+
934
+ this.loaded = true;
935
+ this.log('debug', '${className} widget UI created');
936
+
937
+ // Start auto-refresh
938
+ this.startAutoRefresh();
939
+
940
+ return this;
941
+ }
942
+
943
+ /**
944
+ * Generate sample data - customize this for your data source
945
+ */
946
+ async getData() {
947
+ // Sample data - replace with actual data fetching
948
+ const sampleData = [
949
+ ['Server 1', 'Online', Math.floor(Math.random() * 100) + '%'],
950
+ ['Server 2', 'Online', Math.floor(Math.random() * 100) + '%'],
951
+ ['Server 3', 'Warning', Math.floor(Math.random() * 100) + '%'],
952
+ ['Server 4', 'Online', Math.floor(Math.random() * 100) + '%'],
953
+ ['Server 5', 'Offline', '0%'],
954
+ ];
955
+
956
+ // Sort data
957
+ const sorted = [...sampleData].sort((a, b) => {
958
+ const aVal = a[this.sortColumn];
959
+ const bVal = b[this.sortColumn];
960
+ const cmp = aVal.localeCompare(bVal);
961
+ return this.sortAsc ? cmp : -cmp;
962
+ });
963
+
964
+ this.data = sorted.slice(0, this.maxRows);
965
+
966
+ return {
967
+ rows: this.data,
968
+ timestamp: new Date().toISOString(),
969
+ };
970
+ }
971
+
972
+ /**
973
+ * Render the table with data
974
+ */
975
+ render(result) {
976
+ if (!this.table || !result) return;
977
+
978
+ this.table.setData({
979
+ headers: this.columns,
980
+ rows: result.rows,
981
+ });
982
+ }
983
+
984
+ /**
985
+ * Start auto-refresh timer
986
+ */
987
+ startAutoRefresh() {
988
+ this.stopAutoRefresh();
989
+
990
+ if (this.refreshInterval > 0) {
991
+ this.refreshTimer = setInterval(async () => {
992
+ try {
993
+ const data = await this.getData();
994
+ this.render(data);
995
+ } catch (err) {
996
+ this.log('error', 'Auto-refresh failed: ' + err.message);
997
+ }
998
+ }, this.refreshInterval);
999
+
1000
+ this.log('debug', 'Auto-refresh started (' + this.refreshInterval + 'ms)');
1001
+ }
1002
+ }
1003
+
1004
+ /**
1005
+ * Stop auto-refresh timer
1006
+ */
1007
+ stopAutoRefresh() {
1008
+ if (this.refreshTimer) {
1009
+ clearInterval(this.refreshTimer);
1010
+ this.refreshTimer = null;
1011
+ this.log('debug', 'Auto-refresh stopped');
1012
+ }
1013
+ }
1014
+
1015
+ /**
1016
+ * Destroy the widget
1017
+ */
1018
+ async destroy() {
1019
+ this.stopAutoRefresh();
1020
+
1021
+ if (this.table) {
1022
+ this.table.destroy();
1023
+ this.table = null;
1024
+ }
1025
+
1026
+ if (this.box) {
1027
+ this.box.destroy();
1028
+ this.box = null;
1029
+ }
1030
+
1031
+ this.loaded = false;
1032
+ this.log('info', '${className} widget destroyed');
1033
+ }
1034
+ }
1035
+
1036
+ export { ${className} };
1037
+ `,
1038
+ },
1039
+
1040
+ gauge: {
1041
+ name: 'Gauge Widget',
1042
+ description: 'Widget that displays a circular or linear gauge for single metrics',
1043
+ manifest: (id, name, author, options = {}) => ({
1044
+ id,
1045
+ name: name || id.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
1046
+ description: options.description || 'A widget that displays metrics with a gauge',
1047
+ version: '1.0.0',
1048
+ author: author || '',
1049
+ category: options.category || 'monitoring',
1050
+ type: 'widget',
1051
+ lazyLoad: true,
1052
+ priority: 100,
1053
+ config: {
1054
+ gaugeType: 'circle',
1055
+ minValue: 0,
1056
+ maxValue: 100,
1057
+ refreshInterval: 2000,
1058
+ unit: '%',
1059
+ },
1060
+ __version: 1,
1061
+ }),
1062
+
1063
+ widgetCode: (id, className) => `/**
1064
+ * ${className} Widget Plugin
1065
+ * Gauge widget for displaying single metrics
1066
+ */
1067
+
1068
+ import { BaseWidget } from 'claw-dashboard/widgets';
1069
+
1070
+ /**
1071
+ * ${className} - Gauge widget for Claw Dashboard
1072
+ */
1073
+ export default class ${className} extends BaseWidget {
1074
+ constructor(options = {}) {
1075
+ super(options);
1076
+ this.name = options.name || '${className}';
1077
+ this.description = options.description || 'Gauge widget';
1078
+
1079
+ // Gauge configuration
1080
+ this.gaugeType = this.config.gaugeType || 'circle';
1081
+ this.minValue = this.config.minValue || 0;
1082
+ this.maxValue = this.config.maxValue || 100;
1083
+ this.refreshInterval = this.config.refreshInterval || 2000;
1084
+ this.unit = this.config.unit || '%';
1085
+
1086
+ // Widget state
1087
+ this.gauge = null;
1088
+ this.refreshTimer = null;
1089
+ this.currentValue = 0;
1090
+ }
1091
+
1092
+ /**
1093
+ * Initialize the widget
1094
+ */
1095
+ async init() {
1096
+ this.log('info', '${className} widget initialized');
1097
+ return true;
1098
+ }
1099
+
1100
+ /**
1101
+ * Create the widget UI
1102
+ * @param {Object} screen - Blessed screen object
1103
+ * @param {Object} theme - Theme colors
1104
+ */
1105
+ async create(screen, theme = {}) {
1106
+ const C = theme.colors || {};
1107
+ const blessed = await import('blessed');
1108
+
1109
+ this.screen = screen;
1110
+ this.theme = theme;
1111
+
1112
+ // Main container
1113
+ this.box = blessed.default.box({
1114
+ parent: screen,
1115
+ width: '30%',
1116
+ height: 10,
1117
+ border: { type: 'line' },
1118
+ label: ' ${className.toUpperCase()} ',
1119
+ style: {
1120
+ border: { fg: C.cyan || 'cyan' },
1121
+ },
1122
+ });
1123
+
1124
+ // Create ASCII gauge area
1125
+ this.gaugeArea = blessed.default.text({
1126
+ parent: this.box,
1127
+ top: 1,
1128
+ left: 'center',
1129
+ width: '90%',
1130
+ height: 6,
1131
+ tags: true,
1132
+ style: { fg: C.green || 'green' },
1133
+ });
1134
+
1135
+ // Value display
1136
+ this.valueText = blessed.default.text({
1137
+ parent: this.box,
1138
+ bottom: 0,
1139
+ left: 'center',
1140
+ content: '0' + this.unit,
1141
+ style: { fg: C.white || 'white', bold: true },
1142
+ });
1143
+
1144
+ this.loaded = true;
1145
+ this.log('debug', '${className} widget UI created');
1146
+
1147
+ // Start auto-refresh
1148
+ this.startAutoRefresh();
1149
+
1150
+ return this;
1151
+ }
1152
+
1153
+ /**
1154
+ * Generate ASCII gauge
1155
+ */
1156
+ _renderAsciiGauge(percent, width = 20) {
1157
+ const filled = Math.round((percent / 100) * width);
1158
+ const bar = '█'.repeat(filled) + '░'.repeat(width - filled);
1159
+ return bar;
1160
+ }
1161
+
1162
+ /**
1163
+ * Generate sample data - customize this for your data source
1164
+ */
1165
+ async getData() {
1166
+ // Sample data - replace with actual data fetching
1167
+ const value = Math.floor(Math.random() * (this.maxValue - this.minValue) + this.minValue);
1168
+ this.currentValue = value;
1169
+
1170
+ return {
1171
+ value: value,
1172
+ percentage: ((value - this.minValue) / (this.maxValue - this.minValue)) * 100,
1173
+ timestamp: new Date().toISOString(),
1174
+ };
1175
+ }
1176
+
1177
+ /**
1178
+ * Render the gauge with data
1179
+ */
1180
+ render(result) {
1181
+ if (!this.gaugeArea || !result) return;
1182
+
1183
+ // Generate ASCII gauge
1184
+ const asciiGauge = this._renderAsciiGauge(result.percentage);
1185
+
1186
+ // Color based on value
1187
+ let color = 'green';
1188
+ if (result.percentage > 80) {
1189
+ color = 'red';
1190
+ } else if (result.percentage > 60) {
1191
+ color = 'yellow';
1192
+ }
1193
+
1194
+ // Update gauge display
1195
+ this.gaugeArea.setContent('{' + color + '-fg}' + asciiGauge + '{/' + color + '-fg}');
1196
+
1197
+ // Update value text
1198
+ this.valueText.setContent(result.value + this.unit);
1199
+ this.valueText.style.fg = this.theme?.colors?.[color] || color;
1200
+ }
1201
+
1202
+ /**
1203
+ * Start auto-refresh timer
1204
+ */
1205
+ startAutoRefresh() {
1206
+ this.stopAutoRefresh();
1207
+
1208
+ if (this.refreshInterval > 0) {
1209
+ this.refreshTimer = setInterval(async () => {
1210
+ try {
1211
+ const data = await this.getData();
1212
+ this.render(data);
1213
+ } catch (err) {
1214
+ this.log('error', 'Auto-refresh failed: ' + err.message);
1215
+ }
1216
+ }, this.refreshInterval);
1217
+
1218
+ this.log('debug', 'Auto-refresh started (' + this.refreshInterval + 'ms)');
1219
+ }
1220
+ }
1221
+
1222
+ /**
1223
+ * Stop auto-refresh timer
1224
+ */
1225
+ stopAutoRefresh() {
1226
+ if (this.refreshTimer) {
1227
+ clearInterval(this.refreshTimer);
1228
+ this.refreshTimer = null;
1229
+ this.log('debug', 'Auto-refresh stopped');
1230
+ }
1231
+ }
1232
+
1233
+ /**
1234
+ * Destroy the widget
1235
+ */
1236
+ async destroy() {
1237
+ this.stopAutoRefresh();
1238
+
1239
+ if (this.gaugeArea) {
1240
+ this.gaugeArea = null;
1241
+ }
1242
+
1243
+ if (this.box) {
1244
+ this.box.destroy();
1245
+ this.box = null;
1246
+ }
1247
+
1248
+ this.loaded = false;
1249
+ this.log('info', '${className} widget destroyed');
1250
+ }
1251
+ }
1252
+
1253
+ export { ${className} };
1254
+ `,
1255
+ },
1256
+
1257
+ logViewer: {
1258
+ name: 'Log Viewer Widget',
1259
+ description: 'Widget that displays scrolling log entries with filtering',
1260
+ manifest: (id, name, author, options = {}) => ({
1261
+ id,
1262
+ name: name || id.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
1263
+ description: options.description || 'A widget that displays scrolling log entries',
1264
+ version: '1.0.0',
1265
+ author: author || '',
1266
+ category: options.category || 'monitoring',
1267
+ type: 'widget',
1268
+ lazyLoad: true,
1269
+ priority: 100,
1270
+ config: {
1271
+ maxLines: 50,
1272
+ showTimestamp: true,
1273
+ filterLevels: ['info', 'warn', 'error'],
1274
+ refreshInterval: 1000,
1275
+ },
1276
+ __version: 1,
1277
+ }),
1278
+
1279
+ widgetCode: (id, className) => `/**
1280
+ * ${className} Widget Plugin
1281
+ * Log viewer widget for displaying scrolling log entries
1282
+ */
1283
+
1284
+ import { BaseWidget } from 'claw-dashboard/widgets';
1285
+
1286
+ /**
1287
+ * ${className} - Log viewer widget for Claw Dashboard
1288
+ */
1289
+ export default class ${className} extends BaseWidget {
1290
+ constructor(options = {}) {
1291
+ super(options);
1292
+ this.name = options.name || '${className}';
1293
+ this.description = options.description || 'Log viewer widget';
1294
+
1295
+ // Log viewer configuration
1296
+ this.maxLines = this.config.maxLines || 50;
1297
+ this.showTimestamp = this.config.showTimestamp !== false;
1298
+ this.filterLevels = this.config.filterLevels || ['info', 'warn', 'error'];
1299
+ this.refreshInterval = this.config.refreshInterval || 1000;
1300
+
1301
+ // Widget state
1302
+ this.logBox = null;
1303
+ this.refreshTimer = null;
1304
+ this.logEntries = [];
1305
+ }
1306
+
1307
+ /**
1308
+ * Initialize the widget
1309
+ */
1310
+ async init() {
1311
+ this.log('info', '${className} widget initialized');
1312
+
1313
+ // Add initial log entries
1314
+ this.addLogEntry('info', 'Log viewer initialized');
1315
+ this.addLogEntry('info', 'Waiting for log data...');
1316
+
1317
+ return true;
1318
+ }
1319
+
1320
+ /**
1321
+ * Add a log entry
1322
+ * @param {string} level - Log level (info, warn, error)
1323
+ * @param {string} message - Log message
1324
+ */
1325
+ addLogEntry(level, message) {
1326
+ const entry = {
1327
+ level,
1328
+ message,
1329
+ timestamp: new Date(),
1330
+ };
1331
+
1332
+ this.logEntries.push(entry);
1333
+
1334
+ // Trim to max lines
1335
+ if (this.logEntries.length > this.maxLines) {
1336
+ this.logEntries.shift();
1337
+ }
1338
+ }
1339
+
1340
+ /**
1341
+ * Create the widget UI
1342
+ * @param {Object} screen - Blessed screen object
1343
+ * @param {Object} theme - Theme colors
1344
+ */
1345
+ async create(screen, theme = {}) {
1346
+ const C = theme.colors || {};
1347
+ const blessed = await import('blessed');
1348
+
1349
+ this.screen = screen;
1350
+ this.theme = theme;
1351
+
1352
+ // Main container
1353
+ this.box = blessed.default.box({
1354
+ parent: screen,
1355
+ width: '70%',
1356
+ height: 15,
1357
+ border: { type: 'line' },
1358
+ label: ' ${className.toUpperCase()} ',
1359
+ style: {
1360
+ border: { fg: C.cyan || 'cyan' },
1361
+ },
1362
+ });
1363
+
1364
+ // Log entries box with scrolling
1365
+ this.logBox = blessed.default.log({
1366
+ parent: this.box,
1367
+ top: 1,
1368
+ left: 1,
1369
+ width: '98%',
1370
+ height: '90%',
1371
+ scrollable: true,
1372
+ scrollbar: {
1373
+ style: {
1374
+ bg: C.gray || 'gray',
1375
+ },
1376
+ },
1377
+ style: {
1378
+ fg: C.white || 'white',
1379
+ bg: C.black || 'black',
1380
+ },
1381
+ });
1382
+
1383
+ this.loaded = true;
1384
+ this.log('debug', '${className} widget UI created');
1385
+
1386
+ // Initial render
1387
+ this.renderLogs();
1388
+
1389
+ // Start auto-refresh
1390
+ this.startAutoRefresh();
1391
+
1392
+ return this;
1393
+ }
1394
+
1395
+ /**
1396
+ * Get filtered log entries
1397
+ */
1398
+ async getData() {
1399
+ // Sample log generation - replace with actual log fetching
1400
+ const levels = ['info', 'info', 'info', 'warn', 'error'];
1401
+ const messages = [
1402
+ 'Request processed successfully',
1403
+ 'Connection established',
1404
+ 'Data synchronized',
1405
+ 'High memory usage detected',
1406
+ 'Failed to connect to service',
1407
+ ];
1408
+
1409
+ // Randomly add new log entry
1410
+ if (Math.random() > 0.7) {
1411
+ const level = levels[Math.floor(Math.random() * levels.length)];
1412
+ const message = messages[Math.floor(Math.random() * messages.length)];
1413
+ this.addLogEntry(level, message);
1414
+ }
1415
+
1416
+ // Filter by level
1417
+ const filtered = this.logEntries.filter(entry =>
1418
+ this.filterLevels.includes(entry.level)
1419
+ );
1420
+
1421
+ return {
1422
+ entries: filtered,
1423
+ timestamp: new Date().toISOString(),
1424
+ };
1425
+ }
1426
+
1427
+ /**
1428
+ * Render the log entries
1429
+ */
1430
+ renderLogs() {
1431
+ if (!this.logBox) return;
1432
+
1433
+ this.logBox.setContent('');
1434
+
1435
+ for (const entry of this.logEntries) {
1436
+ if (!this.filterLevels.includes(entry.level)) continue;
1437
+
1438
+ let line = '';
1439
+
1440
+ if (this.showTimestamp) {
1441
+ const time = entry.timestamp.toLocaleTimeString();
1442
+ line += '[' + time + '] ';
1443
+ }
1444
+
1445
+ const levelStr = entry.level.toUpperCase().padEnd(5);
1446
+ line += '[' + levelStr + '] ' + entry.message;
1447
+
1448
+ // Set color based on level
1449
+ const colorMap = {
1450
+ info: this.theme?.colors?.white || 'white',
1451
+ warn: this.theme?.colors?.yellow || 'yellow',
1452
+ error: this.theme?.colors?.red || 'red',
1453
+ };
1454
+
1455
+ this.logBox.add(line, colorMap[entry.level] || 'white');
1456
+ }
1457
+
1458
+ // Scroll to bottom
1459
+ this.logBox.setScrollPerc(100);
1460
+ }
1461
+
1462
+ /**
1463
+ * Render the widget with data
1464
+ */
1465
+ render(result) {
1466
+ if (!result) return;
1467
+ this.renderLogs();
1468
+ }
1469
+
1470
+ /**
1471
+ * Start auto-refresh timer
1472
+ */
1473
+ startAutoRefresh() {
1474
+ this.stopAutoRefresh();
1475
+
1476
+ if (this.refreshInterval > 0) {
1477
+ this.refreshTimer = setInterval(async () => {
1478
+ try {
1479
+ const data = await this.getData();
1480
+ this.render(data);
1481
+ } catch (err) {
1482
+ this.log('error', 'Auto-refresh failed: ' + err.message);
1483
+ }
1484
+ }, this.refreshInterval);
1485
+
1486
+ this.log('debug', 'Auto-refresh started (' + this.refreshInterval + 'ms)');
1487
+ }
1488
+ }
1489
+
1490
+ /**
1491
+ * Stop auto-refresh timer
1492
+ */
1493
+ stopAutoRefresh() {
1494
+ if (this.refreshTimer) {
1495
+ clearInterval(this.refreshTimer);
1496
+ this.refreshTimer = null;
1497
+ this.log('debug', 'Auto-refresh stopped');
1498
+ }
1499
+ }
1500
+
1501
+ /**
1502
+ * Destroy the widget
1503
+ */
1504
+ async destroy() {
1505
+ this.stopAutoRefresh();
1506
+
1507
+ if (this.logBox) {
1508
+ this.logBox.destroy();
1509
+ this.logBox = null;
1510
+ }
1511
+
1512
+ if (this.box) {
1513
+ this.box.destroy();
1514
+ this.box = null;
1515
+ }
1516
+
1517
+ this.logEntries = [];
1518
+ this.loaded = false;
1519
+ this.log('info', '${className} widget destroyed');
1520
+ }
1521
+ }
1522
+
1523
+ export { ${className} };
1524
+ `,
1525
+ },
1526
+ };
1527
+
1528
+ /**
1529
+ * Converts a plugin ID to a valid JavaScript class name
1530
+ * @param {string} id - Plugin ID (e.g., "my-custom-widget")
1531
+ * @returns {string} Class name (e.g., "MyCustomWidget")
1532
+ */
1533
+ function toClassName(id) {
1534
+ return id
1535
+ .split(/[-_]/)
1536
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
1537
+ .join('');
1538
+ }
1539
+
1540
+ /**
1541
+ * Validates a plugin ID
1542
+ * @param {string} id - Plugin ID
1543
+ * @returns {object} Validation result
1544
+ */
1545
+ function validatePluginId(id) {
1546
+ if (!id || typeof id !== 'string') {
1547
+ return { valid: false, error: 'Plugin ID must be a non-empty string' };
1548
+ }
1549
+
1550
+ if (id.length < 1 || id.length > 64) {
1551
+ return { valid: false, error: 'Plugin ID must be between 1 and 64 characters' };
1552
+ }
1553
+
1554
+ // Check for valid characters
1555
+ const validPattern = /^[a-zA-Z0-9]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$/;
1556
+ if (!validPattern.test(id)) {
1557
+ return {
1558
+ valid: false,
1559
+ error: 'Plugin ID must contain only alphanumeric characters, hyphens, and underscores, and cannot start or end with a hyphen/underscore',
1560
+ };
1561
+ }
1562
+
1563
+ // Check for reserved names
1564
+ const reservedNames = ['claw', 'dashboard', 'admin', 'system', 'test'];
1565
+ if (reservedNames.includes(id.toLowerCase())) {
1566
+ return { valid: false, error: `'${id}' is a reserved name` };
1567
+ }
1568
+
1569
+ return { valid: true };
1570
+ }
1571
+
1572
+ /**
1573
+ * Generate README content
1574
+ * @param {string} id - Plugin ID
1575
+ * @param {string} name - Plugin name
1576
+ * @param {string} templateType - Template type
1577
+ * @returns {string} README content
1578
+ */
1579
+ function generateReadme(id, name, templateType) {
1580
+ const template = TEMPLATES[templateType] || TEMPLATES.basic;
1581
+ return `# ${name}
1582
+
1583
+ ${template.description} for Claw Dashboard.
1584
+
1585
+ ## Installation
1586
+
1587
+ 1. Copy this directory to your Claw Dashboard plugins folder:
1588
+ \`\`\`bash
1589
+ cp -r ${id} ~/.openclaw/plugins/
1590
+ \`\`\`
1591
+
1592
+ 2. Restart Claw Dashboard or reload plugins
1593
+
1594
+ ## Configuration
1595
+
1596
+ Edit \`plugin.json\` to customize the widget:
1597
+
1598
+ \`\`\`json
1599
+ {
1600
+ "config": {
1601
+ // See plugin.json for available options
1602
+ }
1603
+ }
1604
+ \`\`\`
1605
+
1606
+ ## Development
1607
+
1608
+ ### File Structure
1609
+
1610
+ \`\`\`
1611
+ ${id}/
1612
+ ├── plugin.json # Plugin manifest
1613
+ ├── index.js # Widget code
1614
+ └── README.md # This file
1615
+ \`\`\`
1616
+
1617
+ ### Testing
1618
+
1619
+ Run your widget in Claw Dashboard:
1620
+
1621
+ \`\`\`bash
1622
+ clawdash --debug
1623
+ \`\`\`
1624
+
1625
+ ## API Reference
1626
+
1627
+ See [Claw Dashboard Plugin Documentation](https://github.com/spleck/claw-dashboard/blob/main/docs/PLUGINS.md) for full API reference.
1628
+
1629
+ ## License
1630
+
1631
+ MIT
1632
+ `;
1633
+ }
1634
+
1635
+ /**
1636
+ * List available templates
1637
+ * @returns {object[]} Array of template info
1638
+ */
1639
+ export function listTemplates() {
1640
+ return Object.entries(TEMPLATES).map(([key, template]) => ({
1641
+ id: key,
1642
+ name: template.name,
1643
+ description: template.description,
1644
+ }));
1645
+ }
1646
+
1647
+ /**
1648
+ * Creates a plugin scaffold
1649
+ * @param {string} id - Plugin ID
1650
+ * @param {object} options - Options
1651
+ * @returns {object} Result
1652
+ */
1653
+ export async function createPlugin(id, options = {}) {
1654
+ const {
1655
+ name,
1656
+ author,
1657
+ outputDir,
1658
+ template = 'basic',
1659
+ dryRun = false,
1660
+ force = false,
1661
+ } = options;
1662
+
1663
+ // Validate plugin ID
1664
+ const validation = validatePluginId(id);
1665
+ if (!validation.valid) {
1666
+ return {
1667
+ success: false,
1668
+ error: validation.error,
1669
+ code: 'INVALID_ID',
1670
+ };
1671
+ }
1672
+
1673
+ // Validate template
1674
+ const selectedTemplate = TEMPLATES[template];
1675
+ if (!selectedTemplate) {
1676
+ return {
1677
+ success: false,
1678
+ error: `Unknown template: ${template}. Available: ${Object.keys(TEMPLATES).join(', ')}`,
1679
+ code: 'INVALID_TEMPLATE',
1680
+ };
1681
+ }
1682
+
1683
+ // Determine output directory
1684
+ const pluginsDir = outputDir || join(homedir(), '.openclaw', 'plugins');
1685
+ const pluginDir = join(pluginsDir, id);
1686
+
1687
+ // Check if plugin already exists
1688
+ if (existsSync(pluginDir) && !force) {
1689
+ return {
1690
+ success: false,
1691
+ error: `Plugin directory already exists: ${pluginDir}`,
1692
+ code: 'ALREADY_EXISTS',
1693
+ path: pluginDir,
1694
+ };
1695
+ }
1696
+
1697
+ // Generate class name from ID
1698
+ const className = toClassName(id);
1699
+
1700
+ // Generate files
1701
+ const files = {
1702
+ 'plugin.json': JSON.stringify(
1703
+ selectedTemplate.manifest(id, name, author, options),
1704
+ null,
1705
+ 2
1706
+ ),
1707
+ 'index.js': selectedTemplate.widgetCode(id, className),
1708
+ 'README.md': generateReadme(id, name || id, template),
1709
+ };
1710
+
1711
+ // In dry-run mode, just return what would be created
1712
+ if (dryRun) {
1713
+ return {
1714
+ success: true,
1715
+ dryRun: true,
1716
+ path: pluginDir,
1717
+ files: Object.keys(files),
1718
+ template,
1719
+ };
1720
+ }
1721
+
1722
+ // Create directory
1723
+ try {
1724
+ mkdirSync(pluginDir, { recursive: true });
1725
+ } catch (err) {
1726
+ return {
1727
+ success: false,
1728
+ error: `Failed to create directory: ${err.message}`,
1729
+ code: 'MKDIR_ERROR',
1730
+ };
1731
+ }
1732
+
1733
+ // Write files
1734
+ const createdFiles = [];
1735
+ for (const [filename, content] of Object.entries(files)) {
1736
+ const filePath = join(pluginDir, filename);
1737
+ try {
1738
+ writeFileSync(filePath, content);
1739
+ createdFiles.push(filename);
1740
+ } catch (err) {
1741
+ return {
1742
+ success: false,
1743
+ error: `Failed to write ${filename}: ${err.message}`,
1744
+ code: 'WRITE_ERROR',
1745
+ path: filePath,
1746
+ };
1747
+ }
1748
+ }
1749
+
1750
+ return {
1751
+ success: true,
1752
+ path: pluginDir,
1753
+ files: createdFiles,
1754
+ id,
1755
+ className,
1756
+ template,
1757
+ };
1758
+ }
1759
+
1760
+ /**
1761
+ * Main CLI handler
1762
+ * @param {string[]} args - CLI arguments
1763
+ * @returns {number} Exit code
1764
+ */
1765
+ export async function runScaffoldCli(args) {
1766
+ const command = args[0];
1767
+
1768
+ if (!command || command === '--help' || command === '-h') {
1769
+ console.log(`
1770
+ Plugin Scaffolding CLI for Claw Dashboard
1771
+
1772
+ Usage: clawdash create-plugin <id> [options]
1773
+
1774
+ Arguments:
1775
+ id Plugin ID (kebab-case, e.g., "my-custom-widget")
1776
+
1777
+ Options:
1778
+ -t, --template Template to use (basic, api, chart, table, gauge, logViewer)
1779
+ Default: basic
1780
+ -n, --name Display name for the widget
1781
+ -a, --author Author name or email
1782
+ -c, --category Widget category (system, monitoring, custom, example)
1783
+ Default: custom
1784
+ --desc Widget description
1785
+ -o, --output Output directory (default: ~/.openclaw/plugins/)
1786
+ -f, --force Overwrite existing plugin
1787
+ --dry-run Show what would be created without creating it
1788
+ --list-templates Show available templates
1789
+ -i, --interactive Start interactive mode (prompts for all options)
1790
+ -h, --help Show this help message
1791
+
1792
+ Examples:
1793
+ clawdash create-plugin my-widget
1794
+ clawdash create-plugin api-status --template api --author "John Doe"
1795
+ clawdash create-plugin metrics --template chart --category monitoring
1796
+ clawdash create-plugin my-widget --interactive
1797
+ clawdash create-plugin --list-templates
1798
+ `);
1799
+ return 0;
1800
+ }
1801
+
1802
+ if (command === '--version' || command === '-v') {
1803
+ console.log('clawdash-create-plugin 1.1.0');
1804
+ return 0;
1805
+ }
1806
+
1807
+ if (command === '--list-templates' || command === 'list-templates') {
1808
+ console.log('Available Templates:');
1809
+ console.log('');
1810
+ const templates = listTemplates();
1811
+ templates.forEach(t => {
1812
+ console.log(` ${t.id.padEnd(12)} ${t.name}`);
1813
+ console.log(` ${t.description}`);
1814
+ console.log('');
1815
+ });
1816
+ return 0;
1817
+ }
1818
+
1819
+ // Parse arguments
1820
+ const options = {
1821
+ name: undefined,
1822
+ author: undefined,
1823
+ category: 'custom',
1824
+ description: undefined,
1825
+ template: 'basic',
1826
+ outputDir: undefined,
1827
+ force: false,
1828
+ dryRun: false,
1829
+ };
1830
+
1831
+ let pluginId = null;
1832
+
1833
+ for (let i = 0; i < args.length; i++) {
1834
+ const arg = args[i];
1835
+
1836
+ if (!arg.startsWith('-') && !pluginId) {
1837
+ pluginId = arg;
1838
+ continue;
1839
+ }
1840
+
1841
+ switch (arg) {
1842
+ case '-t':
1843
+ case '--template':
1844
+ options.template = args[++i];
1845
+ break;
1846
+ case '-n':
1847
+ case '--name':
1848
+ options.name = args[++i];
1849
+ break;
1850
+ case '-a':
1851
+ case '--author':
1852
+ options.author = args[++i];
1853
+ break;
1854
+ case '-c':
1855
+ case '--category':
1856
+ options.category = args[++i];
1857
+ break;
1858
+ case '--desc':
1859
+ options.description = args[++i];
1860
+ break;
1861
+ case '-o':
1862
+ case '--output':
1863
+ options.outputDir = args[++i];
1864
+ break;
1865
+ case '-f':
1866
+ case '--force':
1867
+ options.force = true;
1868
+ break;
1869
+ case '--dry-run':
1870
+ options.dryRun = true;
1871
+ break;
1872
+ case '-i':
1873
+ case '--interactive':
1874
+ options.interactive = true;
1875
+ break;
1876
+ case '-h':
1877
+ case '--help':
1878
+ // Already handled above
1879
+ break;
1880
+ }
1881
+ }
1882
+
1883
+ // Handle interactive mode
1884
+ if (options.interactive) {
1885
+ const interactiveOptions = await runInteractiveMode();
1886
+ if (!interactiveOptions) {
1887
+ return 0; // User cancelled
1888
+ }
1889
+ // Merge interactive options with any passed CLI options
1890
+ Object.assign(options, interactiveOptions);
1891
+ pluginId = interactiveOptions.id;
1892
+ }
1893
+
1894
+ if (!pluginId) {
1895
+ console.error('Error: Plugin ID is required');
1896
+ console.error('Run with --help for usage information');
1897
+ return 1;
1898
+ }
1899
+
1900
+ const result = await createPlugin(pluginId, options);
1901
+
1902
+ if (!result.success) {
1903
+ console.error(`Error: ${result.error}`);
1904
+ return 1;
1905
+ }
1906
+
1907
+ if (result.dryRun) {
1908
+ console.log('Dry run - would create:');
1909
+ console.log(` Directory: ${result.path}`);
1910
+ console.log(` Template: ${result.template}`);
1911
+ console.log(' Files:');
1912
+ result.files.forEach(f => console.log(` - ${f}`));
1913
+ } else {
1914
+ console.log(`✓ Created plugin: ${pluginId}`);
1915
+ console.log(` Path: ${result.path}`);
1916
+ console.log(` Template: ${result.template}`);
1917
+ console.log(` Files: ${result.files.join(', ')}`);
1918
+ console.log('');
1919
+ console.log('Next steps:');
1920
+ console.log(` 1. Edit ${result.path}/index.js to implement your widget`);
1921
+ console.log(` 2. Update ${result.path}/plugin.json with your configuration`);
1922
+ console.log(' 3. Run clawdash to see your widget in action');
1923
+ }
1924
+
1925
+ return 0;
1926
+ }
1927
+
1928
+ // Run if called directly
1929
+ if (import.meta.url === `file://${process.argv[1]}`) {
1930
+ (async () => {
1931
+ const exitCode = await runScaffoldCli(process.argv.slice(2));
1932
+ process.exit(exitCode);
1933
+ })();
1934
+ }