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,372 @@
1
+ /**
2
+ * Dashboard Snapshot Module
3
+ * Provides export/import functionality for dashboard configurations
4
+ * Allows users to share layouts and backup settings
5
+ */
6
+
7
+ import fs from 'fs';
8
+ import os from 'os';
9
+ import { join } from 'path';
10
+ import { PATHS, DEFAULT_SETTINGS, DASHBOARD_VERSION } from './config.js';
11
+ import logger from './logger.js';
12
+
13
+ /**
14
+ * Snapshot schema version for compatibility checking
15
+ */
16
+ export const SNAPSHOT_SCHEMA_VERSION = '1.0.0';
17
+
18
+ /**
19
+ * Settings keys to export (for a clean, portable snapshot)
20
+ * Excludes: sessionSearchQuery, firstRun (runtime state)
21
+ */
22
+ const EXPORTABLE_SETTINGS = [
23
+ 'refreshInterval',
24
+ 'logLevelFilter',
25
+ 'sessionSortMode',
26
+ 'showWidget1',
27
+ 'showWidget2',
28
+ 'showWidget3',
29
+ 'showWidget4',
30
+ 'showWidget5',
31
+ 'showWidget6',
32
+ 'showWidget7',
33
+ 'showWidget8',
34
+ 'showWidget9',
35
+ 'showPerformanceMetrics',
36
+ 'theme',
37
+ 'exportFormat',
38
+ 'exportDirectory',
39
+ 'favorites',
40
+ 'showFavoritesOnly',
41
+ 'gatewayEndpoints',
42
+ 'activeGatewayEndpoint',
43
+ 'webInterface',
44
+ 'widgetLoading',
45
+ 'plugins',
46
+ 'autoRetry',
47
+ ];
48
+
49
+ /**
50
+ * Create a snapshot from current settings
51
+ * @param {Object} currentSettings - Current dashboard settings
52
+ * @param {Object} options - Snapshot options
53
+ * @param {string} [options.name] - Optional snapshot name
54
+ * @param {string} [options.description] - Optional description
55
+ * @returns {Object} Snapshot object
56
+ */
57
+ export function createSnapshot(currentSettings, options = {}) {
58
+ const snapshot = {
59
+ schemaVersion: SNAPSHOT_SCHEMA_VERSION,
60
+ dashboardVersion: DASHBOARD_VERSION,
61
+ createdAt: new Date().toISOString(),
62
+ name: options.name || 'Dashboard Snapshot',
63
+ description: options.description || '',
64
+ platform: {
65
+ os: os.platform(),
66
+ arch: os.arch(),
67
+ nodeVersion: process.version,
68
+ },
69
+ settings: {},
70
+ };
71
+
72
+ // Extract only exportable settings
73
+ for (const key of EXPORTABLE_SETTINGS) {
74
+ if (key in currentSettings) {
75
+ snapshot.settings[key] = currentSettings[key];
76
+ }
77
+ }
78
+
79
+ // Add metadata about what's included
80
+ snapshot.metadata = {
81
+ widgetCount: [
82
+ 'showWidget1', 'showWidget2', 'showWidget3', 'showWidget4',
83
+ 'showWidget5', 'showWidget6', 'showWidget7', 'showWidget8', 'showWidget9',
84
+ ].filter(w => snapshot.settings[w] !== false).length,
85
+ pluginCount: Object.keys(snapshot.settings.plugins || {}).length,
86
+ endpointCount: (snapshot.settings.gatewayEndpoints || []).length,
87
+ };
88
+
89
+ return snapshot;
90
+ }
91
+
92
+ /**
93
+ * Validate a snapshot before importing
94
+ * @param {Object} snapshot - Snapshot to validate
95
+ * @returns {Object} Validation result { valid: boolean, error?: string }
96
+ */
97
+ export function validateSnapshot(snapshot) {
98
+ if (!snapshot || typeof snapshot !== 'object') {
99
+ return { valid: false, error: 'Invalid snapshot format' };
100
+ }
101
+
102
+ // Check schema version (allow same major version)
103
+ const schemaVersion = snapshot.schemaVersion || '0.0.0';
104
+ const [major] = schemaVersion.split('.');
105
+ const [currentMajor] = SNAPSHOT_SCHEMA_VERSION.split('.');
106
+
107
+ if (parseInt(major) > parseInt(currentMajor)) {
108
+ return {
109
+ valid: false,
110
+ error: `Snapshot version ${schemaVersion} is newer than supported (${SNAPSHOT_SCHEMA_VERSION})`
111
+ };
112
+ }
113
+
114
+ // Validate settings object exists
115
+ if (!snapshot.settings || typeof snapshot.settings !== 'object') {
116
+ return { valid: false, error: 'Missing or invalid settings in snapshot' };
117
+ }
118
+
119
+ // Validate critical settings have correct types
120
+ const validations = [
121
+ { key: 'refreshInterval', type: 'number', min: 500, max: 60000 },
122
+ { key: 'theme', type: 'string', allowed: ['auto', 'default', 'dark', 'high-contrast', 'ocean'] },
123
+ { key: 'logLevelFilter', type: 'string', allowed: ['all', 'debug', 'info', 'warn', 'error'] },
124
+ ];
125
+
126
+ for (const v of validations) {
127
+ const value = snapshot.settings[v.key];
128
+ if (value !== undefined) {
129
+ if (v.type && typeof value !== v.type) {
130
+ return { valid: false, error: `Invalid type for ${v.key}: expected ${v.type}` };
131
+ }
132
+ if (v.min !== undefined && value < v.min) {
133
+ return { valid: false, error: `${v.key} must be at least ${v.min}` };
134
+ }
135
+ if (v.max !== undefined && value > v.max) {
136
+ return { valid: false, error: `${v.key} must be at most ${v.max}` };
137
+ }
138
+ if (v.allowed && !v.allowed.includes(value)) {
139
+ return { valid: false, error: `${v.key} must be one of: ${v.allowed.join(', ')}` };
140
+ }
141
+ }
142
+ }
143
+
144
+ // Validate widget booleans
145
+ for (let i = 1; i <= 9; i++) {
146
+ const key = `showWidget${i}`;
147
+ const value = snapshot.settings[key];
148
+ if (value !== undefined && typeof value !== 'boolean') {
149
+ return { valid: false, error: `${key} must be a boolean` };
150
+ }
151
+ }
152
+
153
+ return { valid: true };
154
+ }
155
+
156
+ /**
157
+ * Merge snapshot settings with defaults (for safe import)
158
+ * Preserves existing settings not in snapshot, applies snapshot settings
159
+ * @param {Object} existingSettings - Current settings
160
+ * @param {Object} snapshotSettings - Settings from snapshot
161
+ * @returns {Object} Merged settings
162
+ */
163
+ export function mergeSnapshotSettings(existingSettings, snapshotSettings) {
164
+ const merged = { ...existingSettings };
165
+
166
+ // Apply snapshot settings
167
+ for (const key of EXPORTABLE_SETTINGS) {
168
+ if (key in snapshotSettings) {
169
+ // Deep clone to avoid reference issues
170
+ if (typeof snapshotSettings[key] === 'object' && snapshotSettings[key] !== null) {
171
+ merged[key] = JSON.parse(JSON.stringify(snapshotSettings[key]));
172
+ } else {
173
+ merged[key] = snapshotSettings[key];
174
+ }
175
+ }
176
+ }
177
+
178
+ return merged;
179
+ }
180
+
181
+ /**
182
+ * Export snapshot to file
183
+ * @param {Object} snapshot - Snapshot object
184
+ * @param {string} filePath - Target file path
185
+ * @returns {Object} Result { success: boolean, error?: string, path?: string }
186
+ */
187
+ export function exportSnapshotToFile(snapshot, filePath) {
188
+ try {
189
+ const dir = filePath.substring(0, filePath.lastIndexOf('/'));
190
+ if (dir && !fs.existsSync(dir)) {
191
+ fs.mkdirSync(dir, { recursive: true });
192
+ }
193
+
194
+ fs.writeFileSync(filePath, JSON.stringify(snapshot, null, 2));
195
+
196
+ // Set secure permissions (owner read/write only)
197
+ try {
198
+ fs.chmodSync(filePath, 0o600);
199
+ } catch (permErr) {
200
+ logger.warn(`Could not set permissions on snapshot: ${permErr.message}`);
201
+ }
202
+
203
+ return { success: true, path: filePath };
204
+ } catch (err) {
205
+ return { success: false, error: err.message };
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Import snapshot from file
211
+ * @param {string} filePath - Path to snapshot file
212
+ * @returns {Object} Result { success: boolean, error?: string, snapshot?: Object }
213
+ */
214
+ export function importSnapshotFromFile(filePath) {
215
+ try {
216
+ if (!fs.existsSync(filePath)) {
217
+ return { success: false, error: `File not found: ${filePath}` };
218
+ }
219
+
220
+ const data = fs.readFileSync(filePath, 'utf8');
221
+ const snapshot = JSON.parse(data);
222
+
223
+ const validation = validateSnapshot(snapshot);
224
+ if (!validation.valid) {
225
+ return { success: false, error: validation.error };
226
+ }
227
+
228
+ return { success: true, snapshot };
229
+ } catch (err) {
230
+ if (err instanceof SyntaxError) {
231
+ return { success: false, error: 'Invalid JSON format' };
232
+ }
233
+ return { success: false, error: err.message };
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Generate default snapshot filename
239
+ * @param {string} [name] - Optional name to include in filename
240
+ * @returns {string} Generated filename
241
+ */
242
+ export function generateSnapshotFilename(name) {
243
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
244
+ const safeName = name ? name.replace(/[^a-zA-Z0-9-_]/g, '_') : 'dashboard';
245
+ return `claw-snapshot-${safeName}-${timestamp}.json`;
246
+ }
247
+
248
+ /**
249
+ * Get snapshots directory
250
+ * @returns {string} Path to snapshots directory
251
+ */
252
+ export function getSnapshotsDirectory() {
253
+ return join(PATHS.OPENCLAW_DIR, 'snapshots');
254
+ }
255
+
256
+ /**
257
+ * List available snapshots
258
+ * @returns {Array} Array of snapshot info objects
259
+ */
260
+ export function listSnapshots() {
261
+ const dir = getSnapshotsDirectory();
262
+ if (!fs.existsSync(dir)) {
263
+ return [];
264
+ }
265
+
266
+ try {
267
+ const files = fs.readdirSync(dir)
268
+ .filter(f => f.endsWith('.json'))
269
+ .map(f => {
270
+ const path = join(dir, f);
271
+ try {
272
+ const data = fs.readFileSync(path, 'utf8');
273
+ const snapshot = JSON.parse(data);
274
+ const stats = fs.statSync(path);
275
+ return {
276
+ filename: f,
277
+ path,
278
+ name: snapshot.name || 'Unnamed',
279
+ description: snapshot.description || '',
280
+ createdAt: snapshot.createdAt || stats.mtime.toISOString(),
281
+ dashboardVersion: snapshot.dashboardVersion || 'unknown',
282
+ schemaVersion: snapshot.schemaVersion || 'unknown',
283
+ metadata: snapshot.metadata || {},
284
+ };
285
+ } catch (err) {
286
+ return null;
287
+ }
288
+ })
289
+ .filter(Boolean)
290
+ .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
291
+
292
+ return files;
293
+ } catch (err) {
294
+ logger.warn(`Failed to list snapshots: ${err.message}`);
295
+ return [];
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Delete a snapshot
301
+ * @param {string} filename - Snapshot filename
302
+ * @returns {Object} Result { success: boolean, error?: string }
303
+ */
304
+ export function deleteSnapshot(filename) {
305
+ const dir = getSnapshotsDirectory();
306
+ const filePath = join(dir, filename);
307
+
308
+ // Security: ensure the resolved path is within snapshots directory
309
+ const resolvedPath = fs.realpathSync.safe
310
+ ? fs.realpathSync(filePath)
311
+ : filePath;
312
+
313
+ if (!resolvedPath.startsWith(dir)) {
314
+ return { success: false, error: 'Invalid snapshot path' };
315
+ }
316
+
317
+ try {
318
+ if (!fs.existsSync(filePath)) {
319
+ return { success: false, error: 'Snapshot not found' };
320
+ }
321
+ fs.unlinkSync(filePath);
322
+ return { success: true };
323
+ } catch (err) {
324
+ return { success: false, error: err.message };
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Get snapshot summary for display
330
+ * @param {Object} snapshot - Snapshot object
331
+ * @returns {string} Formatted summary
332
+ */
333
+ export function getSnapshotSummary(snapshot) {
334
+ if (!snapshot) return 'Invalid snapshot';
335
+
336
+ const lines = [
337
+ `Name: ${snapshot.name || 'Unnamed'}`,
338
+ `Created: ${snapshot.createdAt ? new Date(snapshot.createdAt).toLocaleString() : 'Unknown'}`,
339
+ ];
340
+
341
+ if (snapshot.description) {
342
+ lines.push(`Description: ${snapshot.description}`);
343
+ }
344
+
345
+ if (snapshot.metadata) {
346
+ const { widgetCount, pluginCount, endpointCount } = snapshot.metadata;
347
+ lines.push(`Widgets: ${widgetCount}, Plugins: ${pluginCount}, Endpoints: ${endpointCount}`);
348
+ }
349
+
350
+ if (snapshot.settings) {
351
+ const theme = snapshot.settings.theme || 'auto';
352
+ const refresh = snapshot.settings.refreshInterval || 2000;
353
+ lines.push(`Theme: ${theme}, Refresh: ${refresh}ms`);
354
+ }
355
+
356
+ return lines.join('\n');
357
+ }
358
+
359
+ export default {
360
+ SNAPSHOT_SCHEMA_VERSION,
361
+ EXPORTABLE_SETTINGS,
362
+ createSnapshot,
363
+ validateSnapshot,
364
+ mergeSnapshotSettings,
365
+ exportSnapshotToFile,
366
+ importSnapshotFromFile,
367
+ generateSnapshotFilename,
368
+ getSnapshotsDirectory,
369
+ listSnapshots,
370
+ deleteSnapshot,
371
+ getSnapshotSummary,
372
+ };
package/src/splash.js ADDED
@@ -0,0 +1,115 @@
1
+ import blessed from 'blessed';
2
+ import { DASHBOARD_VERSION } from './config.js';
3
+
4
+ /**
5
+ * Shows a fullscreen splash screen with loading progress
6
+ * @param {blessed.Screen} screen - The blessed screen
7
+ * @param {Function} loadFn - Async function that performs loading
8
+ * @returns {Promise<void>}
9
+ */
10
+ export async function showSplashScreen(screen, loadFn) {
11
+ // Create fullscreen overlay
12
+ const splashBox = blessed.box({
13
+ parent: screen,
14
+ top: 0,
15
+ left: 0,
16
+ width: '100%',
17
+ height: '100%',
18
+ style: { bg: 'black' }
19
+ });
20
+
21
+ // Simple centered text - no ASCII art, just basic characters
22
+ blessed.text({
23
+ parent: splashBox,
24
+ top: Math.floor(screen.height / 2) - 4,
25
+ left: 'center',
26
+ content: 'CLAW DASHBOARD',
27
+ style: { fg: 'brightCyan', bold: true }
28
+ });
29
+
30
+ blessed.text({
31
+ parent: splashBox,
32
+ top: Math.floor(screen.height / 2) - 2,
33
+ left: 'center',
34
+ content: '================',
35
+ style: { fg: 'cyan' }
36
+ });
37
+
38
+ blessed.text({
39
+ parent: splashBox,
40
+ top: Math.floor(screen.height / 2),
41
+ left: 'center',
42
+ content: `v${DASHBOARD_VERSION}`,
43
+ style: { fg: 'gray' }
44
+ });
45
+
46
+ // Status line
47
+ const statusText = blessed.text({
48
+ parent: splashBox,
49
+ top: Math.floor(screen.height / 2) + 3,
50
+ left: 'center',
51
+ width: 35,
52
+ height: 1,
53
+ content: 'Initializing...',
54
+ style: { fg: 'white', bold: true },
55
+ align: 'center'
56
+ });
57
+
58
+ // Spinner next to status
59
+ const spinnerText = blessed.text({
60
+ parent: splashBox,
61
+ top: Math.floor(screen.height / 2) + 3,
62
+ left: Math.floor(screen.width / 2) + 18,
63
+ content: '*',
64
+ style: { fg: 'brightGreen', bold: true }
65
+ });
66
+
67
+ // Progress bar
68
+ const progressBar = blessed.text({
69
+ parent: splashBox,
70
+ top: Math.floor(screen.height / 2) + 5,
71
+ left: 'center',
72
+ width: 30,
73
+ content: '[----------------------]',
74
+ style: { fg: 'gray' },
75
+ align: 'center'
76
+ });
77
+
78
+ screen.render();
79
+
80
+ // Spinner animation
81
+ const spinnerFrames = ['|', '/', '-', '\\\\'];
82
+ let spinnerIdx = 0;
83
+ const spinnerInterval = setInterval(() => {
84
+ spinnerIdx = (spinnerIdx + 1) % spinnerFrames.length;
85
+ spinnerText.setContent(spinnerFrames[spinnerIdx]);
86
+ screen.render();
87
+ }, 150);
88
+
89
+ // Update progress function
90
+ const updateProgress = (progress, status) => {
91
+ if (status) statusText.setContent(status);
92
+
93
+ const filled = Math.floor((progress / 100) * 22);
94
+ const bar = '[' + '#'.repeat(filled) + '-'.repeat(22 - filled) + ']';
95
+ progressBar.setContent(bar);
96
+ progressBar.style.fg = progress < 50 ? 'red' : progress < 80 ? 'yellow' : 'green';
97
+
98
+ screen.render();
99
+ };
100
+
101
+ // Execute loading
102
+ try {
103
+ if (loadFn) {
104
+ await loadFn(updateProgress);
105
+ }
106
+ updateProgress(100, 'Ready!');
107
+ await new Promise(r => setTimeout(r, 400));
108
+ } finally {
109
+ clearInterval(spinnerInterval);
110
+ splashBox.destroy();
111
+ screen.render();
112
+ }
113
+ }
114
+
115
+ export default { showSplashScreen };