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,499 @@
1
+ /**
2
+ * Container Environment Detection Module
3
+ * Detects if running inside Docker, Kubernetes, or other containerized environments
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import os from 'os';
8
+ import { exec } from 'child_process';
9
+ import { promisify } from 'util';
10
+ import logger from './logger.js';
11
+
12
+ const execAsync = promisify(exec);
13
+
14
+ /**
15
+ * Container environment types
16
+ * @typedef {Object} ContainerEnvironment
17
+ * @property {boolean} isContainer - Whether running in any container
18
+ * @property {boolean} isDocker - Whether running in Docker
19
+ * @property {boolean} isKubernetes - Whether running in Kubernetes
20
+ * @property {boolean} isWSL - Whether running in Windows Subsystem for Linux
21
+ * @property {number} wslVersion - WSL version (1 or 2) if isWSL is true, 0 otherwise
22
+ * @property {string|null} wslDistro - WSL distribution name if available
23
+ * @property {string|null} containerId - Container ID if detected
24
+ * @property {string|null} containerName - Container name if available
25
+ * @property {string|null} podName - Kubernetes pod name if available
26
+ * @property {string|null} namespace - Kubernetes namespace if available
27
+ * @property {string} runtime - Detected container runtime (docker, containerd, cri-o, etc.)
28
+ */
29
+
30
+ /**
31
+ * Default container environment state
32
+ * @type {ContainerEnvironment}
33
+ */
34
+ const DEFAULT_CONTAINER_ENV = {
35
+ isContainer: false,
36
+ isDocker: false,
37
+ isKubernetes: false,
38
+ isWSL: false,
39
+ wslVersion: 0,
40
+ wslDistro: null,
41
+ containerId: null,
42
+ containerName: null,
43
+ podName: null,
44
+ namespace: null,
45
+ runtime: 'none'
46
+ };
47
+
48
+ /**
49
+ * Cached container environment detection result
50
+ * @type {ContainerEnvironment|null}
51
+ */
52
+ let cachedContainerEnv = null;
53
+ let cacheTimestamp = 0;
54
+ const CACHE_TTL_MS = 30000; // 30 seconds cache
55
+
56
+ /**
57
+ * Check if running inside a Docker container by examining cgroup
58
+ * @returns {Promise<boolean>}
59
+ */
60
+ async function checkDockerCgroup() {
61
+ try {
62
+ const cgroupContent = fs.readFileSync('/proc/self/cgroup', 'utf8');
63
+ return cgroupContent.includes('docker') ||
64
+ cgroupContent.includes('containerd') ||
65
+ cgroupContent.includes('crio') ||
66
+ /[0-9a-f]{64}/.test(cgroupContent); // Container ID pattern
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Check if .dockerenv file exists (older Docker detection method)
74
+ * @returns {boolean}
75
+ */
76
+ function checkDockerEnvFile() {
77
+ try {
78
+ fs.accessSync('/.dockerenv', fs.constants.F_OK);
79
+ return true;
80
+ } catch {
81
+ return false;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Check for Kubernetes-specific environment variables and files
87
+ * @returns {Promise<{isKubernetes: boolean, podName: string|null, namespace: string|null}>}
88
+ */
89
+ async function checkKubernetes() {
90
+ const result = {
91
+ isKubernetes: false,
92
+ podName: null,
93
+ namespace: null
94
+ };
95
+
96
+ try {
97
+ // Check for Kubernetes service account directory
98
+ if (fs.existsSync('/var/run/secrets/kubernetes.io')) {
99
+ result.isKubernetes = true;
100
+ }
101
+
102
+ // Check environment variables
103
+ if (process.env.KUBERNETES_SERVICE_HOST || process.env.KUBERNETES_PORT) {
104
+ result.isKubernetes = true;
105
+ }
106
+
107
+ // Get pod name from hostname (typically pod name in K8s)
108
+ if (process.env.HOSTNAME) {
109
+ // K8s hostnames often contain pod names
110
+ const hostname = process.env.HOSTNAME;
111
+ if (hostname.includes('-') && /[a-f0-9]{5,10}$/.test(hostname)) {
112
+ result.podName = hostname;
113
+ }
114
+ }
115
+
116
+ // Get namespace from service account namespace file
117
+ try {
118
+ const namespacePath = '/var/run/secrets/kubernetes.io/serviceaccount/namespace';
119
+ if (fs.existsSync(namespacePath)) {
120
+ result.namespace = fs.readFileSync(namespacePath, 'utf8').trim();
121
+ }
122
+ } catch {
123
+ // Ignore read errors
124
+ }
125
+
126
+ // Also check env var
127
+ if (!result.namespace && process.env.KUBERNETES_NAMESPACE) {
128
+ result.namespace = process.env.KUBERNETES_NAMESPACE;
129
+ }
130
+ } catch {
131
+ // Ignore errors
132
+ }
133
+
134
+ return result;
135
+ }
136
+
137
+ /**
138
+ * Check if running in Windows Subsystem for Linux (WSL)
139
+ * @returns {boolean}
140
+ */
141
+ function checkWSL() {
142
+ // Check for WSL in /proc/version
143
+ try {
144
+ const version = fs.readFileSync('/proc/version', 'utf8').toLowerCase();
145
+ if (version.includes('microsoft') || version.includes('wsl')) {
146
+ return true;
147
+ }
148
+ } catch {
149
+ // Ignore errors
150
+ }
151
+
152
+ // Check for WSL-specific environment variable
153
+ if (process.env.WSL_DISTRO_NAME || process.env.WSLENV) {
154
+ return true;
155
+ }
156
+
157
+ // Check for Windows interop path
158
+ try {
159
+ if (fs.existsSync('/mnt/c/Windows')) {
160
+ return true;
161
+ }
162
+ } catch {
163
+ // Ignore errors
164
+ }
165
+
166
+ return false;
167
+ }
168
+
169
+ /**
170
+ * Detect WSL version (1 or 2)
171
+ * WSL1: Uses Windows kernel, no systemd, /proc/version has "Microsoft" but no "WSL2"
172
+ * WSL2: Uses Linux kernel in VM, has systemd, /proc/version has "WSL2" or "microsoft-standard-WSL2"
173
+ * @returns {number} WSL version (1 or 2), 0 if not in WSL
174
+ */
175
+ function detectWSLVersion() {
176
+ if (!checkWSL()) {
177
+ return 0;
178
+ }
179
+
180
+ // Check /proc/version for WSL2-specific strings
181
+ try {
182
+ const version = fs.readFileSync('/proc/version', 'utf8').toLowerCase();
183
+ // WSL2 typically has "wsl2" or "microsoft-standard-wsl2" in the version string
184
+ if (version.includes('wsl2') || version.includes('microsoft-standard')) {
185
+ return 2;
186
+ }
187
+ } catch {
188
+ // Ignore errors
189
+ }
190
+
191
+ // Check if systemd is available (WSL2 usually has systemd, WSL1 does not)
192
+ try {
193
+ if (fs.existsSync('/run/systemd/system')) {
194
+ return 2;
195
+ }
196
+ } catch {
197
+ // Ignore errors
198
+ }
199
+
200
+ // Check for WSL2-specific kernel features
201
+ // WSL2 kernels typically have version 4.19.x or higher
202
+ try {
203
+ const version = fs.readFileSync('/proc/version', 'utf8');
204
+ const kernelMatch = version.match(/Linux version (\d+)\.(\d+)/);
205
+ if (kernelMatch) {
206
+ const major = parseInt(kernelMatch[1]);
207
+ const minor = parseInt(kernelMatch[2]);
208
+ // WSL2 typically has kernel 4.19+ or 5.x, while WSL1 uses a simulated 2.6 or 3.x
209
+ if (major > 4 || (major === 4 && minor >= 19)) {
210
+ return 2;
211
+ }
212
+ }
213
+ } catch {
214
+ // Ignore errors
215
+ }
216
+
217
+ // If we detected WSL but not WSL2 specific markers, it's likely WSL1
218
+ return 1;
219
+ }
220
+
221
+ /**
222
+ * Get the WSL distribution name
223
+ * @returns {string|null}
224
+ */
225
+ function getWSLDistroName() {
226
+ // Check WSL_DISTRO_NAME environment variable (available in WSL2)
227
+ if (process.env.WSL_DISTRO_NAME) {
228
+ return process.env.WSL_DISTRO_NAME;
229
+ }
230
+
231
+ // Try to get from /etc/os-release
232
+ try {
233
+ const osRelease = fs.readFileSync('/etc/os-release', 'utf8');
234
+ const nameMatch = osRelease.match(/PRETTY_NAME="([^"]+)"/);
235
+ if (nameMatch) {
236
+ return nameMatch[1];
237
+ }
238
+ } catch {
239
+ // Ignore errors
240
+ }
241
+
242
+ return null;
243
+ }
244
+
245
+ /**
246
+ * Get container ID from cgroup
247
+ * @returns {string|null}
248
+ */
249
+ function getContainerId() {
250
+ try {
251
+ const cgroupContent = fs.readFileSync('/proc/self/cgroup', 'utf8');
252
+
253
+ // Try to match container ID pattern (64 hex characters)
254
+ const match = cgroupContent.match(/[0-9a-f]{64}/);
255
+ if (match) {
256
+ return match[0].substring(0, 12); // Short form like Docker
257
+ }
258
+
259
+ // Try alternative patterns for different container runtimes
260
+ const lines = cgroupContent.split('\n');
261
+ for (const line of lines) {
262
+ // Match patterns like /docker/<id> or /containerd/<id>
263
+ const dockerMatch = line.match(/\/docker\/([0-9a-f]{12,64})/i);
264
+ if (dockerMatch) {
265
+ return dockerMatch[1].substring(0, 12);
266
+ }
267
+
268
+ const containerdMatch = line.match(/\/containerd\/.*\/([0-9a-f]{12,64})/i);
269
+ if (containerdMatch) {
270
+ return containerdMatch[1].substring(0, 12);
271
+ }
272
+
273
+ const criMatch = line.match(/\/cri-containerd\/([0-9a-f]{12,64})/i);
274
+ if (criMatch) {
275
+ return criMatch[1].substring(0, 12);
276
+ }
277
+ }
278
+ } catch {
279
+ // Ignore errors
280
+ }
281
+
282
+ return null;
283
+ }
284
+
285
+ /**
286
+ * Get container name from Docker if available
287
+ * @returns {Promise<string|null>}
288
+ */
289
+ async function getContainerName() {
290
+ // Try environment variable first
291
+ if (process.env.CONTAINER_NAME) {
292
+ return process.env.CONTAINER_NAME;
293
+ }
294
+
295
+ if (process.env.HOSTNAME && !checkWSL()) {
296
+ // In containers, hostname is often the container ID
297
+ // But could also be a custom name
298
+ return process.env.HOSTNAME;
299
+ }
300
+
301
+ return null;
302
+ }
303
+
304
+ /**
305
+ * Detect container runtime from cgroup or process tree
306
+ * @returns {Promise<string>}
307
+ */
308
+ async function detectRuntime() {
309
+ try {
310
+ const cgroupContent = fs.readFileSync('/proc/self/cgroup', 'utf8');
311
+
312
+ if (cgroupContent.includes('docker')) {
313
+ return 'docker';
314
+ }
315
+ if (cgroupContent.includes('containerd')) {
316
+ return 'containerd';
317
+ }
318
+ if (cgroupContent.includes('crio')) {
319
+ return 'cri-o';
320
+ }
321
+ if (cgroupContent.includes('podman')) {
322
+ return 'podman';
323
+ }
324
+ if (cgroupContent.includes('lxc')) {
325
+ return 'lxc';
326
+ }
327
+ if (cgroupContent.includes('systemd-nspawn')) {
328
+ return 'systemd-nspawn';
329
+ }
330
+
331
+ // Check if any container-specific files exist
332
+ if (fs.existsSync('/run/containerd')) {
333
+ return 'containerd';
334
+ }
335
+ if (fs.existsSync('/run/crio')) {
336
+ return 'cri-o';
337
+ }
338
+ if (fs.existsSync('/run/docker.sock') || fs.existsSync('/var/run/docker.sock')) {
339
+ // Can access Docker socket (might be mounted in container)
340
+ return 'docker-accessible';
341
+ }
342
+
343
+ // Default to generic container if cgroup suggests it
344
+ if (cgroupContent.includes('0::/') && cgroupContent.split('\n').length > 1) {
345
+ return 'container';
346
+ }
347
+ } catch {
348
+ // Ignore errors
349
+ }
350
+
351
+ return 'unknown';
352
+ }
353
+
354
+ /**
355
+ * Detect containerized environment
356
+ * @returns {Promise<ContainerEnvironment>}
357
+ */
358
+ export async function detectContainerEnv() {
359
+ // Return cached result if still valid
360
+ const now = Date.now();
361
+ if (cachedContainerEnv && (now - cacheTimestamp) < CACHE_TTL_MS) {
362
+ return cachedContainerEnv;
363
+ }
364
+
365
+ const env = { ...DEFAULT_CONTAINER_ENV };
366
+
367
+ // Skip detection on Windows (not containers in the Linux sense)
368
+ const platform = os.platform();
369
+ if (platform === 'win32') {
370
+ return env;
371
+ }
372
+
373
+ try {
374
+ // Check for Docker
375
+ const [dockerCgroup, dockerEnvFile] = await Promise.all([
376
+ checkDockerCgroup(),
377
+ Promise.resolve(checkDockerEnvFile())
378
+ ]);
379
+
380
+ if (dockerCgroup || dockerEnvFile) {
381
+ env.isContainer = true;
382
+ env.isDocker = true;
383
+ env.containerId = getContainerId();
384
+ env.containerName = await getContainerName();
385
+ }
386
+
387
+ // Check for Kubernetes
388
+ const k8sInfo = await checkKubernetes();
389
+ if (k8sInfo.isKubernetes) {
390
+ env.isContainer = true;
391
+ env.isKubernetes = true;
392
+ env.podName = k8sInfo.podName;
393
+ env.namespace = k8sInfo.namespace;
394
+ }
395
+
396
+ // Check for WSL and detect version
397
+ env.isWSL = checkWSL();
398
+ if (env.isWSL) {
399
+ env.wslVersion = detectWSLVersion();
400
+ env.wslDistro = getWSLDistroName();
401
+ }
402
+
403
+ // Detect runtime if in a container
404
+ if (env.isContainer) {
405
+ env.runtime = await detectRuntime();
406
+ }
407
+
408
+ // Cache result
409
+ cachedContainerEnv = env;
410
+ cacheTimestamp = now;
411
+
412
+ } catch (err) {
413
+ logger.warn(`Container detection failed: ${err.message}`);
414
+ }
415
+
416
+ return env;
417
+ }
418
+
419
+ /**
420
+ * Get a human-readable description of the container environment
421
+ * @param {ContainerEnvironment} env
422
+ * @returns {string}
423
+ */
424
+ export function getContainerDescription(env) {
425
+ if (!env.isContainer) {
426
+ return 'Bare Metal/VM';
427
+ }
428
+
429
+ const parts = [];
430
+
431
+ if (env.isKubernetes) {
432
+ parts.push('Kubernetes');
433
+ if (env.namespace) {
434
+ parts.push(`ns:${env.namespace}`);
435
+ }
436
+ if (env.podName) {
437
+ const shortPod = env.podName.length > 20
438
+ ? env.podName.substring(0, 17) + '...'
439
+ : env.podName;
440
+ parts.push(`pod:${shortPod}`);
441
+ }
442
+ } else if (env.isDocker) {
443
+ parts.push('Docker');
444
+ if (env.containerId) {
445
+ parts.push(`id:${env.containerId}`);
446
+ }
447
+ } else {
448
+ parts.push(env.runtime !== 'unknown' ? env.runtime : 'Container');
449
+ }
450
+
451
+ if (env.isWSL) {
452
+ const wslLabel = env.wslVersion === 2 ? 'WSL2' : env.wslVersion === 1 ? 'WSL1' : 'WSL';
453
+ parts.push(`(${wslLabel})`);
454
+ }
455
+
456
+ return parts.join(' ');
457
+ }
458
+
459
+ /**
460
+ * Get short container indicator for system widget
461
+ * @param {ContainerEnvironment} env
462
+ * @returns {string}
463
+ */
464
+ export function getContainerIndicator(env) {
465
+ // Show WSL indicator if running in WSL (even without container)
466
+ if (env.isWSL) {
467
+ const wslLabel = env.wslVersion === 2 ? 'WSL2' : env.wslVersion === 1 ? 'WSL1' : 'WSL';
468
+ return `⊞ ${wslLabel}`;
469
+ }
470
+
471
+ if (!env.isContainer) {
472
+ return '';
473
+ }
474
+
475
+ if (env.isKubernetes) {
476
+ return '☸ K8s';
477
+ }
478
+ if (env.isDocker) {
479
+ return '🐳 Docker';
480
+ }
481
+ return '⬡ Container';
482
+ }
483
+
484
+ /**
485
+ * Clear the container detection cache
486
+ * Forces fresh detection on next call
487
+ */
488
+ export function clearContainerCache() {
489
+ cachedContainerEnv = null;
490
+ cacheTimestamp = 0;
491
+ }
492
+
493
+ export default {
494
+ detectContainerEnv,
495
+ getContainerDescription,
496
+ getContainerIndicator,
497
+ clearContainerCache,
498
+ DEFAULT_CONTAINER_ENV
499
+ };