@girardmedia/bootspring 1.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 (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +255 -0
  3. package/agents/README.md +93 -0
  4. package/agents/api-expert/context.md +416 -0
  5. package/agents/architecture-expert/context.md +454 -0
  6. package/agents/backend-expert/context.md +483 -0
  7. package/agents/code-review-expert/context.md +365 -0
  8. package/agents/database-expert/context.md +250 -0
  9. package/agents/devops-expert/context.md +446 -0
  10. package/agents/frontend-expert/context.md +364 -0
  11. package/agents/index.js +140 -0
  12. package/agents/performance-expert/context.md +377 -0
  13. package/agents/security-expert/context.md +343 -0
  14. package/agents/testing-expert/context.md +414 -0
  15. package/agents/ui-ux-expert/context.md +448 -0
  16. package/agents/vercel-expert/context.md +426 -0
  17. package/bin/bootspring.js +310 -0
  18. package/cli/agent.js +337 -0
  19. package/cli/context.js +194 -0
  20. package/cli/dashboard.js +150 -0
  21. package/cli/generate.js +294 -0
  22. package/cli/init.js +410 -0
  23. package/cli/loop.js +421 -0
  24. package/cli/mcp.js +241 -0
  25. package/cli/memory.js +303 -0
  26. package/cli/orchestrator.js +400 -0
  27. package/cli/plugin.js +451 -0
  28. package/cli/quality.js +332 -0
  29. package/cli/skill.js +369 -0
  30. package/cli/task.js +628 -0
  31. package/cli/telemetry.js +114 -0
  32. package/cli/todo.js +614 -0
  33. package/cli/update.js +312 -0
  34. package/core/config.js +245 -0
  35. package/core/context.js +329 -0
  36. package/core/entitlements.js +209 -0
  37. package/core/index.js +43 -0
  38. package/core/policies.js +68 -0
  39. package/core/telemetry.js +247 -0
  40. package/core/utils.js +380 -0
  41. package/dashboard/server.js +818 -0
  42. package/docs/integrations/claude-code.md +42 -0
  43. package/docs/integrations/codex.md +42 -0
  44. package/docs/mcp-api-platform.md +102 -0
  45. package/generators/generate.js +598 -0
  46. package/generators/index.js +18 -0
  47. package/hooks/context-detector.js +177 -0
  48. package/hooks/index.js +35 -0
  49. package/hooks/prompt-enhancer.js +289 -0
  50. package/intelligence/git-memory.js +551 -0
  51. package/intelligence/index.js +59 -0
  52. package/intelligence/orchestrator.js +964 -0
  53. package/intelligence/prd.js +447 -0
  54. package/intelligence/recommendation-weights.json +18 -0
  55. package/intelligence/recommendations.js +234 -0
  56. package/mcp/capabilities.js +71 -0
  57. package/mcp/contracts/mcp-contract.v1.json +497 -0
  58. package/mcp/registry.js +213 -0
  59. package/mcp/response-formatter.js +462 -0
  60. package/mcp/server.js +99 -0
  61. package/mcp/tools/agent-tool.js +137 -0
  62. package/mcp/tools/capabilities-tool.js +54 -0
  63. package/mcp/tools/context-tool.js +49 -0
  64. package/mcp/tools/dashboard-tool.js +58 -0
  65. package/mcp/tools/generate-tool.js +46 -0
  66. package/mcp/tools/loop-tool.js +134 -0
  67. package/mcp/tools/memory-tool.js +180 -0
  68. package/mcp/tools/orchestrator-tool.js +232 -0
  69. package/mcp/tools/plugin-tool.js +76 -0
  70. package/mcp/tools/quality-tool.js +47 -0
  71. package/mcp/tools/skill-tool.js +233 -0
  72. package/mcp/tools/telemetry-tool.js +95 -0
  73. package/mcp/tools/todo-tool.js +133 -0
  74. package/package.json +98 -0
  75. package/plugins/index.js +141 -0
  76. package/quality/index.js +380 -0
  77. package/quality/lint-budgets.json +19 -0
  78. package/skills/index.js +787 -0
  79. package/skills/patterns/README.md +163 -0
  80. package/skills/patterns/api/route-handler.md +217 -0
  81. package/skills/patterns/api/server-action.md +249 -0
  82. package/skills/patterns/auth/clerk.md +132 -0
  83. package/skills/patterns/database/prisma.md +180 -0
  84. package/skills/patterns/payments/stripe.md +272 -0
  85. package/skills/patterns/security/validation.md +268 -0
  86. package/skills/patterns/testing/vitest.md +307 -0
  87. package/templates/bootspring.config.js +83 -0
  88. package/templates/mcp.json +9 -0
@@ -0,0 +1,818 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Bootspring Dashboard Server
5
+ * Real-time project monitoring and management
6
+ *
7
+ * @package bootspring
8
+ * @module dashboard
9
+ */
10
+
11
+ const http = require('http');
12
+ const path = require('path');
13
+ const fs = require('fs');
14
+ const { WebSocketServer } = require('ws');
15
+
16
+ // Configuration
17
+ const DEFAULT_PORT = 3456;
18
+ const port = parseInt(process.env.BOOTSPRING_PORT || DEFAULT_PORT);
19
+
20
+ // Find project root
21
+ function findProjectRoot() {
22
+ let dir = process.cwd();
23
+ while (dir !== path.parse(dir).root) {
24
+ if (fs.existsSync(path.join(dir, 'bootspring.config.js')) ||
25
+ fs.existsSync(path.join(dir, 'package.json'))) {
26
+ return dir;
27
+ }
28
+ dir = path.dirname(dir);
29
+ }
30
+ return process.cwd();
31
+ }
32
+
33
+ const projectRoot = findProjectRoot();
34
+
35
+ // Load project config
36
+ function loadConfig() {
37
+ try {
38
+ const configPath = path.join(projectRoot, 'bootspring.config.js');
39
+ if (fs.existsSync(configPath)) {
40
+ delete require.cache[require.resolve(configPath)];
41
+ return require(configPath);
42
+ }
43
+ } catch (e) {
44
+ // Ignore
45
+ }
46
+ return { project: { name: 'Bootspring Project' } };
47
+ }
48
+
49
+ // Load todos
50
+ function loadTodos() {
51
+ try {
52
+ const todoPath = path.join(projectRoot, 'todo.md');
53
+ if (!fs.existsSync(todoPath)) return [];
54
+
55
+ const content = fs.readFileSync(todoPath, 'utf-8');
56
+ const lines = content.split('\n');
57
+ const todos = [];
58
+
59
+ for (const line of lines) {
60
+ const match = line.match(/^- \[([ x])\] (.+)$/);
61
+ if (match) {
62
+ todos.push({
63
+ done: match[1] === 'x',
64
+ text: match[2].trim()
65
+ });
66
+ }
67
+ }
68
+
69
+ return todos;
70
+ } catch (e) {
71
+ return [];
72
+ }
73
+ }
74
+
75
+ // Get git info
76
+ function getGitInfo() {
77
+ try {
78
+ const { execSync } = require('child_process');
79
+ const branch = execSync('git branch --show-current', { cwd: projectRoot, encoding: 'utf-8' }).trim();
80
+ const status = execSync('git status --porcelain', { cwd: projectRoot, encoding: 'utf-8' });
81
+ const lastCommit = execSync('git log -1 --format="%h %s" 2>/dev/null || echo "No commits"', { cwd: projectRoot, encoding: 'utf-8' }).trim();
82
+
83
+ const changes = status.split('\n').filter(Boolean);
84
+
85
+ return {
86
+ branch,
87
+ lastCommit,
88
+ changedFiles: changes.length,
89
+ status: changes.length === 0 ? 'clean' : 'modified'
90
+ };
91
+ } catch (e) {
92
+ return { branch: 'unknown', lastCommit: '', changedFiles: 0, status: 'unknown' };
93
+ }
94
+ }
95
+
96
+ // Get project stats
97
+ function getStats() {
98
+ const config = loadConfig();
99
+ const todos = loadTodos();
100
+ const git = getGitInfo();
101
+
102
+ return {
103
+ project: config.project || { name: 'Bootspring Project' },
104
+ todos: {
105
+ total: todos.length,
106
+ done: todos.filter(t => t.done).length,
107
+ items: todos
108
+ },
109
+ git,
110
+ timestamp: new Date().toISOString()
111
+ };
112
+ }
113
+
114
+ // Dashboard HTML
115
+ function getDashboardHTML() {
116
+ const config = loadConfig();
117
+ const projectName = config.project?.name || 'Bootspring Project';
118
+
119
+ return `<!DOCTYPE html>
120
+ <html lang="en">
121
+ <head>
122
+ <meta charset="UTF-8">
123
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
124
+ <title>${projectName} - Bootspring Dashboard</title>
125
+ <link rel="preconnect" href="https://fonts.googleapis.com">
126
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
127
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
128
+ <style>
129
+ :root {
130
+ --bg-primary: #0f172a;
131
+ --bg-secondary: #1e293b;
132
+ --bg-tertiary: #334155;
133
+ --text-primary: #f8fafc;
134
+ --text-secondary: #94a3b8;
135
+ --text-muted: #64748b;
136
+ --accent-cyan: #06b6d4;
137
+ --accent-green: #22c55e;
138
+ --accent-yellow: #eab308;
139
+ --accent-red: #ef4444;
140
+ --accent-purple: #a855f7;
141
+ --border: #334155;
142
+ --radius: 12px;
143
+ }
144
+
145
+ * {
146
+ margin: 0;
147
+ padding: 0;
148
+ box-sizing: border-box;
149
+ }
150
+
151
+ body {
152
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
153
+ background: var(--bg-primary);
154
+ color: var(--text-primary);
155
+ min-height: 100vh;
156
+ line-height: 1.6;
157
+ }
158
+
159
+ .container {
160
+ max-width: 1400px;
161
+ margin: 0 auto;
162
+ padding: 2rem;
163
+ }
164
+
165
+ header {
166
+ display: flex;
167
+ align-items: center;
168
+ justify-content: space-between;
169
+ margin-bottom: 2rem;
170
+ padding-bottom: 1.5rem;
171
+ border-bottom: 1px solid var(--border);
172
+ }
173
+
174
+ .logo {
175
+ display: flex;
176
+ align-items: center;
177
+ gap: 1rem;
178
+ }
179
+
180
+ .logo-icon {
181
+ width: 48px;
182
+ height: 48px;
183
+ background: linear-gradient(135deg, var(--accent-cyan), var(--accent-purple));
184
+ border-radius: 12px;
185
+ display: flex;
186
+ align-items: center;
187
+ justify-content: center;
188
+ font-size: 1.5rem;
189
+ }
190
+
191
+ .logo h1 {
192
+ font-size: 1.5rem;
193
+ font-weight: 700;
194
+ background: linear-gradient(135deg, var(--accent-cyan), var(--accent-purple));
195
+ -webkit-background-clip: text;
196
+ -webkit-text-fill-color: transparent;
197
+ }
198
+
199
+ .logo span {
200
+ font-size: 0.875rem;
201
+ color: var(--text-secondary);
202
+ }
203
+
204
+ .status-badge {
205
+ display: flex;
206
+ align-items: center;
207
+ gap: 0.5rem;
208
+ padding: 0.5rem 1rem;
209
+ background: var(--bg-secondary);
210
+ border-radius: 999px;
211
+ font-size: 0.875rem;
212
+ }
213
+
214
+ .status-dot {
215
+ width: 8px;
216
+ height: 8px;
217
+ border-radius: 50%;
218
+ background: var(--accent-green);
219
+ animation: pulse 2s infinite;
220
+ }
221
+
222
+ @keyframes pulse {
223
+ 0%, 100% { opacity: 1; }
224
+ 50% { opacity: 0.5; }
225
+ }
226
+
227
+ .grid {
228
+ display: grid;
229
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
230
+ gap: 1.5rem;
231
+ }
232
+
233
+ .card {
234
+ background: var(--bg-secondary);
235
+ border-radius: var(--radius);
236
+ border: 1px solid var(--border);
237
+ overflow: hidden;
238
+ }
239
+
240
+ .card-header {
241
+ padding: 1rem 1.25rem;
242
+ border-bottom: 1px solid var(--border);
243
+ display: flex;
244
+ align-items: center;
245
+ justify-content: space-between;
246
+ }
247
+
248
+ .card-title {
249
+ font-size: 0.875rem;
250
+ font-weight: 600;
251
+ text-transform: uppercase;
252
+ letter-spacing: 0.05em;
253
+ color: var(--text-secondary);
254
+ }
255
+
256
+ .card-body {
257
+ padding: 1.25rem;
258
+ }
259
+
260
+ .stat-value {
261
+ font-size: 2.5rem;
262
+ font-weight: 700;
263
+ font-family: 'JetBrains Mono', monospace;
264
+ }
265
+
266
+ .stat-label {
267
+ color: var(--text-muted);
268
+ font-size: 0.875rem;
269
+ margin-top: 0.25rem;
270
+ }
271
+
272
+ .todo-list {
273
+ list-style: none;
274
+ max-height: 400px;
275
+ overflow-y: auto;
276
+ }
277
+
278
+ .todo-item {
279
+ display: flex;
280
+ align-items: center;
281
+ gap: 0.75rem;
282
+ padding: 0.75rem 0;
283
+ border-bottom: 1px solid var(--border);
284
+ transition: background 0.2s;
285
+ }
286
+
287
+ .todo-item:last-child {
288
+ border-bottom: none;
289
+ }
290
+
291
+ .todo-item:hover {
292
+ background: var(--bg-tertiary);
293
+ margin: 0 -1.25rem;
294
+ padding-left: 1.25rem;
295
+ padding-right: 1.25rem;
296
+ }
297
+
298
+ .todo-checkbox {
299
+ width: 18px;
300
+ height: 18px;
301
+ border: 2px solid var(--border);
302
+ border-radius: 4px;
303
+ display: flex;
304
+ align-items: center;
305
+ justify-content: center;
306
+ flex-shrink: 0;
307
+ }
308
+
309
+ .todo-item.done .todo-checkbox {
310
+ background: var(--accent-green);
311
+ border-color: var(--accent-green);
312
+ }
313
+
314
+ .todo-item.done .todo-checkbox::after {
315
+ content: '✓';
316
+ color: white;
317
+ font-size: 12px;
318
+ }
319
+
320
+ .todo-item.done .todo-text {
321
+ text-decoration: line-through;
322
+ color: var(--text-muted);
323
+ }
324
+
325
+ .todo-text {
326
+ flex: 1;
327
+ font-size: 0.9375rem;
328
+ }
329
+
330
+ .progress-bar {
331
+ height: 8px;
332
+ background: var(--bg-tertiary);
333
+ border-radius: 4px;
334
+ overflow: hidden;
335
+ margin-top: 1rem;
336
+ }
337
+
338
+ .progress-fill {
339
+ height: 100%;
340
+ background: linear-gradient(90deg, var(--accent-cyan), var(--accent-green));
341
+ border-radius: 4px;
342
+ transition: width 0.5s ease;
343
+ }
344
+
345
+ .git-info {
346
+ display: flex;
347
+ flex-direction: column;
348
+ gap: 0.75rem;
349
+ }
350
+
351
+ .git-row {
352
+ display: flex;
353
+ align-items: center;
354
+ gap: 0.75rem;
355
+ }
356
+
357
+ .git-label {
358
+ color: var(--text-muted);
359
+ font-size: 0.8125rem;
360
+ min-width: 80px;
361
+ }
362
+
363
+ .git-value {
364
+ font-family: 'JetBrains Mono', monospace;
365
+ font-size: 0.875rem;
366
+ color: var(--text-primary);
367
+ }
368
+
369
+ .git-badge {
370
+ padding: 0.25rem 0.75rem;
371
+ border-radius: 999px;
372
+ font-size: 0.75rem;
373
+ font-weight: 500;
374
+ }
375
+
376
+ .git-badge.clean {
377
+ background: rgba(34, 197, 94, 0.2);
378
+ color: var(--accent-green);
379
+ }
380
+
381
+ .git-badge.modified {
382
+ background: rgba(234, 179, 8, 0.2);
383
+ color: var(--accent-yellow);
384
+ }
385
+
386
+ .quick-actions {
387
+ display: grid;
388
+ grid-template-columns: repeat(2, 1fr);
389
+ gap: 0.75rem;
390
+ }
391
+
392
+ .action-btn {
393
+ display: flex;
394
+ align-items: center;
395
+ gap: 0.5rem;
396
+ padding: 0.75rem 1rem;
397
+ background: var(--bg-tertiary);
398
+ border: 1px solid var(--border);
399
+ border-radius: 8px;
400
+ color: var(--text-primary);
401
+ font-size: 0.875rem;
402
+ cursor: pointer;
403
+ transition: all 0.2s;
404
+ text-decoration: none;
405
+ }
406
+
407
+ .action-btn:hover {
408
+ background: var(--accent-cyan);
409
+ border-color: var(--accent-cyan);
410
+ }
411
+
412
+ .action-icon {
413
+ font-size: 1.125rem;
414
+ }
415
+
416
+ .agents-grid {
417
+ display: grid;
418
+ grid-template-columns: repeat(3, 1fr);
419
+ gap: 0.5rem;
420
+ }
421
+
422
+ .agent-chip {
423
+ padding: 0.5rem 0.75rem;
424
+ background: var(--bg-tertiary);
425
+ border-radius: 6px;
426
+ font-size: 0.75rem;
427
+ text-align: center;
428
+ color: var(--text-secondary);
429
+ transition: all 0.2s;
430
+ cursor: pointer;
431
+ }
432
+
433
+ .agent-chip:hover {
434
+ background: var(--accent-purple);
435
+ color: white;
436
+ }
437
+
438
+ .empty-state {
439
+ text-align: center;
440
+ padding: 2rem;
441
+ color: var(--text-muted);
442
+ }
443
+
444
+ .timestamp {
445
+ font-size: 0.75rem;
446
+ color: var(--text-muted);
447
+ font-family: 'JetBrains Mono', monospace;
448
+ }
449
+
450
+ footer {
451
+ margin-top: 3rem;
452
+ padding-top: 1.5rem;
453
+ border-top: 1px solid var(--border);
454
+ text-align: center;
455
+ color: var(--text-muted);
456
+ font-size: 0.875rem;
457
+ }
458
+
459
+ footer a {
460
+ color: var(--accent-cyan);
461
+ text-decoration: none;
462
+ }
463
+
464
+ footer a:hover {
465
+ text-decoration: underline;
466
+ }
467
+
468
+ @media (max-width: 768px) {
469
+ .container {
470
+ padding: 1rem;
471
+ }
472
+
473
+ header {
474
+ flex-direction: column;
475
+ gap: 1rem;
476
+ align-items: flex-start;
477
+ }
478
+
479
+ .grid {
480
+ grid-template-columns: 1fr;
481
+ }
482
+
483
+ .agents-grid {
484
+ grid-template-columns: repeat(2, 1fr);
485
+ }
486
+ }
487
+ </style>
488
+ </head>
489
+ <body>
490
+ <div class="container">
491
+ <header>
492
+ <div class="logo">
493
+ <div class="logo-icon">⚡</div>
494
+ <div>
495
+ <h1>Bootspring</h1>
496
+ <span id="project-name">${projectName}</span>
497
+ </div>
498
+ </div>
499
+ <div class="status-badge">
500
+ <div class="status-dot"></div>
501
+ <span>Live</span>
502
+ <span class="timestamp" id="timestamp"></span>
503
+ </div>
504
+ </header>
505
+
506
+ <div class="grid">
507
+ <!-- Progress Card -->
508
+ <div class="card">
509
+ <div class="card-header">
510
+ <span class="card-title">📊 Progress</span>
511
+ </div>
512
+ <div class="card-body">
513
+ <div class="stat-value" id="progress-value">0%</div>
514
+ <div class="stat-label" id="progress-label">0 of 0 tasks complete</div>
515
+ <div class="progress-bar">
516
+ <div class="progress-fill" id="progress-bar" style="width: 0%"></div>
517
+ </div>
518
+ </div>
519
+ </div>
520
+
521
+ <!-- Git Card -->
522
+ <div class="card">
523
+ <div class="card-header">
524
+ <span class="card-title">🔀 Git Status</span>
525
+ </div>
526
+ <div class="card-body">
527
+ <div class="git-info">
528
+ <div class="git-row">
529
+ <span class="git-label">Branch</span>
530
+ <span class="git-value" id="git-branch">main</span>
531
+ </div>
532
+ <div class="git-row">
533
+ <span class="git-label">Status</span>
534
+ <span class="git-badge clean" id="git-status">clean</span>
535
+ </div>
536
+ <div class="git-row">
537
+ <span class="git-label">Last commit</span>
538
+ <span class="git-value" id="git-commit">...</span>
539
+ </div>
540
+ </div>
541
+ </div>
542
+ </div>
543
+
544
+ <!-- Quick Actions -->
545
+ <div class="card">
546
+ <div class="card-header">
547
+ <span class="card-title">⚡ Quick Actions</span>
548
+ </div>
549
+ <div class="card-body">
550
+ <div class="quick-actions">
551
+ <button class="action-btn" onclick="runCommand('todo list')">
552
+ <span class="action-icon">📝</span>
553
+ <span>View Todos</span>
554
+ </button>
555
+ <button class="action-btn" onclick="runCommand('quality pre-commit')">
556
+ <span class="action-icon">✅</span>
557
+ <span>Quality Check</span>
558
+ </button>
559
+ <button class="action-btn" onclick="runCommand('generate')">
560
+ <span class="action-icon">🔄</span>
561
+ <span>Regenerate</span>
562
+ </button>
563
+ <button class="action-btn" onclick="runCommand('context validate')">
564
+ <span class="action-icon">🔍</span>
565
+ <span>Validate</span>
566
+ </button>
567
+ </div>
568
+ </div>
569
+ </div>
570
+
571
+ <!-- Agents -->
572
+ <div class="card">
573
+ <div class="card-header">
574
+ <span class="card-title">🤖 Agents</span>
575
+ </div>
576
+ <div class="card-body">
577
+ <div class="agents-grid">
578
+ <div class="agent-chip" onclick="invokeAgent('database')">Database</div>
579
+ <div class="agent-chip" onclick="invokeAgent('security')">Security</div>
580
+ <div class="agent-chip" onclick="invokeAgent('frontend')">Frontend</div>
581
+ <div class="agent-chip" onclick="invokeAgent('backend')">Backend</div>
582
+ <div class="agent-chip" onclick="invokeAgent('api')">API</div>
583
+ <div class="agent-chip" onclick="invokeAgent('testing')">Testing</div>
584
+ <div class="agent-chip" onclick="invokeAgent('performance')">Performance</div>
585
+ <div class="agent-chip" onclick="invokeAgent('devops')">DevOps</div>
586
+ <div class="agent-chip" onclick="invokeAgent('architecture')">Architecture</div>
587
+ </div>
588
+ </div>
589
+ </div>
590
+
591
+ <!-- Todo List (spans 2 columns) -->
592
+ <div class="card" style="grid-column: span 2;">
593
+ <div class="card-header">
594
+ <span class="card-title">📋 Todo List</span>
595
+ <span class="timestamp" id="todo-count">0 items</span>
596
+ </div>
597
+ <div class="card-body">
598
+ <ul class="todo-list" id="todo-list">
599
+ <li class="empty-state">No todos yet. Add one with: bootspring todo add "Your task"</li>
600
+ </ul>
601
+ </div>
602
+ </div>
603
+ </div>
604
+
605
+ <footer>
606
+ <p>
607
+ <strong>Bootspring</strong> - Development scaffolding with intelligence
608
+ <br>
609
+ <a href="https://bootspring.com" target="_blank">Documentation</a> ·
610
+ <a href="https://github.com/bootspring/bootspring" target="_blank">GitHub</a>
611
+ </p>
612
+ </footer>
613
+ </div>
614
+
615
+ <script>
616
+ let ws;
617
+ let reconnectAttempts = 0;
618
+
619
+ function connect() {
620
+ ws = new WebSocket('ws://' + window.location.host);
621
+
622
+ ws.onopen = () => {
623
+ console.log('Connected to dashboard');
624
+ reconnectAttempts = 0;
625
+ ws.send(JSON.stringify({ type: 'subscribe' }));
626
+ };
627
+
628
+ ws.onmessage = (event) => {
629
+ const data = JSON.parse(event.data);
630
+ updateDashboard(data);
631
+ };
632
+
633
+ ws.onclose = () => {
634
+ console.log('Disconnected, reconnecting...');
635
+ reconnectAttempts++;
636
+ setTimeout(connect, Math.min(1000 * reconnectAttempts, 5000));
637
+ };
638
+
639
+ ws.onerror = (error) => {
640
+ console.error('WebSocket error:', error);
641
+ };
642
+ }
643
+
644
+ function updateDashboard(data) {
645
+ // Update timestamp
646
+ document.getElementById('timestamp').textContent =
647
+ new Date(data.timestamp).toLocaleTimeString();
648
+
649
+ // Update progress
650
+ const total = data.todos.total || 0;
651
+ const done = data.todos.done || 0;
652
+ const percent = total > 0 ? Math.round((done / total) * 100) : 0;
653
+
654
+ document.getElementById('progress-value').textContent = percent + '%';
655
+ document.getElementById('progress-label').textContent =
656
+ done + ' of ' + total + ' tasks complete';
657
+ document.getElementById('progress-bar').style.width = percent + '%';
658
+
659
+ // Update git info
660
+ document.getElementById('git-branch').textContent = data.git.branch;
661
+ document.getElementById('git-commit').textContent =
662
+ data.git.lastCommit.substring(0, 50) + (data.git.lastCommit.length > 50 ? '...' : '');
663
+
664
+ const statusEl = document.getElementById('git-status');
665
+ statusEl.textContent = data.git.status;
666
+ statusEl.className = 'git-badge ' + data.git.status;
667
+
668
+ // Update todos
669
+ const todoList = document.getElementById('todo-list');
670
+ document.getElementById('todo-count').textContent = total + ' items';
671
+
672
+ if (data.todos.items.length === 0) {
673
+ todoList.innerHTML = '<li class="empty-state">No todos yet. Add one with: bootspring todo add "Your task"</li>';
674
+ } else {
675
+ todoList.innerHTML = data.todos.items.map((todo, i) =>
676
+ '<li class="todo-item ' + (todo.done ? 'done' : '') + '">' +
677
+ '<div class="todo-checkbox"></div>' +
678
+ '<span class="todo-text">' + escapeHtml(todo.text) + '</span>' +
679
+ '</li>'
680
+ ).join('');
681
+ }
682
+ }
683
+
684
+ function escapeHtml(text) {
685
+ const div = document.createElement('div');
686
+ div.textContent = text;
687
+ return div.innerHTML;
688
+ }
689
+
690
+ function runCommand(cmd) {
691
+ alert('Run in terminal:\\nbootspring ' + cmd);
692
+ }
693
+
694
+ function invokeAgent(name) {
695
+ alert('Run in Claude Code:\\n@bootspring agent invoke ' + name + '-expert');
696
+ }
697
+
698
+ // Initial connection
699
+ connect();
700
+
701
+ // Poll for updates every 5 seconds as backup
702
+ setInterval(() => {
703
+ if (ws && ws.readyState === WebSocket.OPEN) {
704
+ ws.send(JSON.stringify({ type: 'refresh' }));
705
+ }
706
+ }, 5000);
707
+ </script>
708
+ </body>
709
+ </html>`;
710
+ }
711
+
712
+ // Create HTTP server
713
+ const server = http.createServer((req, res) => {
714
+ const url = new URL(req.url, `http://localhost:${port}`);
715
+
716
+ // API endpoints
717
+ if (url.pathname === '/api/stats') {
718
+ res.writeHead(200, { 'Content-Type': 'application/json' });
719
+ res.end(JSON.stringify(getStats()));
720
+ return;
721
+ }
722
+
723
+ if (url.pathname === '/api/todos') {
724
+ res.writeHead(200, { 'Content-Type': 'application/json' });
725
+ res.end(JSON.stringify(loadTodos()));
726
+ return;
727
+ }
728
+
729
+ if (url.pathname === '/api/config') {
730
+ res.writeHead(200, { 'Content-Type': 'application/json' });
731
+ res.end(JSON.stringify(loadConfig()));
732
+ return;
733
+ }
734
+
735
+ // Dashboard HTML
736
+ res.writeHead(200, { 'Content-Type': 'text/html' });
737
+ res.end(getDashboardHTML());
738
+ });
739
+
740
+ // Create WebSocket server
741
+ const wss = new WebSocketServer({ server });
742
+
743
+ wss.on('connection', (ws) => {
744
+ console.log('Dashboard client connected');
745
+
746
+ // Send initial data
747
+ ws.send(JSON.stringify(getStats()));
748
+
749
+ // Handle messages
750
+ ws.on('message', (message) => {
751
+ try {
752
+ const data = JSON.parse(message);
753
+
754
+ if (data.type === 'refresh' || data.type === 'subscribe') {
755
+ ws.send(JSON.stringify(getStats()));
756
+ }
757
+ } catch (e) {
758
+ // Ignore parse errors
759
+ }
760
+ });
761
+
762
+ ws.on('close', () => {
763
+ console.log('Dashboard client disconnected');
764
+ });
765
+ });
766
+
767
+ // Broadcast updates to all clients
768
+ function broadcast(data) {
769
+ wss.clients.forEach(client => {
770
+ if (client.readyState === 1) { // WebSocket.OPEN
771
+ client.send(JSON.stringify(data));
772
+ }
773
+ });
774
+ }
775
+
776
+ // Watch for file changes
777
+ const watchFiles = ['todo.md', 'bootspring.config.js'];
778
+ let debounceTimer;
779
+
780
+ for (const file of watchFiles) {
781
+ const filePath = path.join(projectRoot, file);
782
+ if (fs.existsSync(filePath)) {
783
+ fs.watch(filePath, () => {
784
+ clearTimeout(debounceTimer);
785
+ debounceTimer = setTimeout(() => {
786
+ broadcast(getStats());
787
+ }, 100);
788
+ });
789
+ }
790
+ }
791
+
792
+ // Start server
793
+ server.listen(port, () => {
794
+ console.log(`
795
+ \x1b[36m╔══════════════════════════════════════════╗
796
+ ║ ║
797
+ ║ ⚡ Bootspring Dashboard ║
798
+ ║ ║
799
+ ╚══════════════════════════════════════════╝\x1b[0m
800
+
801
+ \x1b[32m✓\x1b[0m Dashboard running at \x1b[36mhttp://localhost:${port}\x1b[0m
802
+ \x1b[32m✓\x1b[0m WebSocket server ready
803
+ \x1b[32m✓\x1b[0m Watching for changes
804
+
805
+ \x1b[2mProject: ${projectRoot}\x1b[0m
806
+ \x1b[2mPress Ctrl+C to stop\x1b[0m
807
+ `);
808
+ });
809
+
810
+ // Handle shutdown
811
+ process.on('SIGINT', () => {
812
+ console.log('\n\x1b[33mShutting down dashboard...\x1b[0m');
813
+ wss.close();
814
+ server.close();
815
+ process.exit(0);
816
+ });
817
+
818
+ module.exports = { server, wss, getStats, broadcast };