tlc-claude-code 1.4.2 → 1.4.5

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 (113) hide show
  1. package/dashboard/dist/App.js +28 -2
  2. package/dashboard/dist/api/health-diagnostics.d.ts +26 -0
  3. package/dashboard/dist/api/health-diagnostics.js +85 -0
  4. package/dashboard/dist/api/health-diagnostics.test.d.ts +1 -0
  5. package/dashboard/dist/api/health-diagnostics.test.js +126 -0
  6. package/dashboard/dist/api/index.d.ts +5 -0
  7. package/dashboard/dist/api/index.js +5 -0
  8. package/dashboard/dist/api/notes-api.d.ts +18 -0
  9. package/dashboard/dist/api/notes-api.js +68 -0
  10. package/dashboard/dist/api/notes-api.test.d.ts +1 -0
  11. package/dashboard/dist/api/notes-api.test.js +113 -0
  12. package/dashboard/dist/api/safeFetch.d.ts +50 -0
  13. package/dashboard/dist/api/safeFetch.js +135 -0
  14. package/dashboard/dist/api/safeFetch.test.d.ts +1 -0
  15. package/dashboard/dist/api/safeFetch.test.js +215 -0
  16. package/dashboard/dist/api/tasks-api.d.ts +32 -0
  17. package/dashboard/dist/api/tasks-api.js +98 -0
  18. package/dashboard/dist/api/tasks-api.test.d.ts +1 -0
  19. package/dashboard/dist/api/tasks-api.test.js +383 -0
  20. package/dashboard/dist/components/BugsPane.d.ts +20 -0
  21. package/dashboard/dist/components/BugsPane.js +210 -0
  22. package/dashboard/dist/components/BugsPane.test.d.ts +1 -0
  23. package/dashboard/dist/components/BugsPane.test.js +256 -0
  24. package/dashboard/dist/components/HealthPane.d.ts +3 -1
  25. package/dashboard/dist/components/HealthPane.js +44 -6
  26. package/dashboard/dist/components/HealthPane.test.js +105 -2
  27. package/dashboard/dist/components/RouterPane.d.ts +4 -3
  28. package/dashboard/dist/components/RouterPane.js +60 -57
  29. package/dashboard/dist/components/RouterPane.test.js +150 -96
  30. package/dashboard/dist/components/UpdateBanner.d.ts +26 -0
  31. package/dashboard/dist/components/UpdateBanner.js +30 -0
  32. package/dashboard/dist/components/UpdateBanner.test.d.ts +1 -0
  33. package/dashboard/dist/components/UpdateBanner.test.js +96 -0
  34. package/dashboard/dist/components/accessibility.test.d.ts +1 -0
  35. package/dashboard/dist/components/accessibility.test.js +116 -0
  36. package/dashboard/dist/components/layout/MobileNav.d.ts +16 -0
  37. package/dashboard/dist/components/layout/MobileNav.js +31 -0
  38. package/dashboard/dist/components/layout/MobileNav.test.d.ts +1 -0
  39. package/dashboard/dist/components/layout/MobileNav.test.js +111 -0
  40. package/dashboard/dist/components/performance.test.d.ts +1 -0
  41. package/dashboard/dist/components/performance.test.js +114 -0
  42. package/dashboard/dist/components/responsive.test.d.ts +1 -0
  43. package/dashboard/dist/components/responsive.test.js +114 -0
  44. package/dashboard/dist/components/ui/Dropdown.d.ts +22 -0
  45. package/dashboard/dist/components/ui/Dropdown.js +109 -0
  46. package/dashboard/dist/components/ui/Dropdown.test.d.ts +1 -0
  47. package/dashboard/dist/components/ui/Dropdown.test.js +105 -0
  48. package/dashboard/dist/components/ui/EmptyState.d.ts +14 -0
  49. package/dashboard/dist/components/ui/EmptyState.js +58 -0
  50. package/dashboard/dist/components/ui/EmptyState.test.d.ts +1 -0
  51. package/dashboard/dist/components/ui/EmptyState.test.js +97 -0
  52. package/dashboard/dist/components/ui/ErrorState.d.ts +17 -0
  53. package/dashboard/dist/components/ui/ErrorState.js +80 -0
  54. package/dashboard/dist/components/ui/ErrorState.test.d.ts +1 -0
  55. package/dashboard/dist/components/ui/ErrorState.test.js +166 -0
  56. package/dashboard/dist/components/ui/Modal.d.ts +13 -0
  57. package/dashboard/dist/components/ui/Modal.js +25 -0
  58. package/dashboard/dist/components/ui/Modal.test.d.ts +1 -0
  59. package/dashboard/dist/components/ui/Modal.test.js +91 -0
  60. package/dashboard/dist/components/ui/Skeleton.d.ts +32 -0
  61. package/dashboard/dist/components/ui/Skeleton.js +48 -0
  62. package/dashboard/dist/components/ui/Skeleton.test.d.ts +1 -0
  63. package/dashboard/dist/components/ui/Skeleton.test.js +125 -0
  64. package/dashboard/dist/components/ui/Toast.d.ts +32 -0
  65. package/dashboard/dist/components/ui/Toast.js +21 -0
  66. package/dashboard/dist/components/ui/Toast.test.d.ts +1 -0
  67. package/dashboard/dist/components/ui/Toast.test.js +118 -0
  68. package/dashboard/dist/hooks/useTheme.d.ts +37 -0
  69. package/dashboard/dist/hooks/useTheme.js +96 -0
  70. package/dashboard/dist/hooks/useTheme.test.d.ts +1 -0
  71. package/dashboard/dist/hooks/useTheme.test.js +94 -0
  72. package/dashboard/dist/hooks/useWebSocket.d.ts +17 -0
  73. package/dashboard/dist/hooks/useWebSocket.js +100 -0
  74. package/dashboard/dist/hooks/useWebSocket.test.d.ts +1 -0
  75. package/dashboard/dist/hooks/useWebSocket.test.js +115 -0
  76. package/dashboard/dist/stores/projectStore.d.ts +44 -0
  77. package/dashboard/dist/stores/projectStore.js +76 -0
  78. package/dashboard/dist/stores/projectStore.test.d.ts +1 -0
  79. package/dashboard/dist/stores/projectStore.test.js +114 -0
  80. package/dashboard/dist/stores/uiStore.d.ts +29 -0
  81. package/dashboard/dist/stores/uiStore.js +72 -0
  82. package/dashboard/dist/stores/uiStore.test.d.ts +1 -0
  83. package/dashboard/dist/stores/uiStore.test.js +93 -0
  84. package/dashboard/package.json +6 -3
  85. package/docker-compose.dev.yml +6 -1
  86. package/package.json +1 -1
  87. package/server/dashboard/index.html +1545 -791
  88. package/server/index.js +64 -0
  89. package/server/lib/api-provider.js +104 -186
  90. package/server/lib/api-provider.test.js +238 -336
  91. package/server/lib/cli-detector.js +90 -166
  92. package/server/lib/cli-detector.test.js +114 -269
  93. package/server/lib/cli-provider.js +142 -212
  94. package/server/lib/cli-provider.test.js +196 -349
  95. package/server/lib/debug.test.js +1 -1
  96. package/server/lib/devserver-router-api.js +54 -249
  97. package/server/lib/devserver-router-api.test.js +126 -426
  98. package/server/lib/introspect.js +309 -0
  99. package/server/lib/introspect.test.js +286 -0
  100. package/server/lib/model-router.js +107 -245
  101. package/server/lib/model-router.test.js +122 -313
  102. package/server/lib/output-schemas.js +146 -269
  103. package/server/lib/output-schemas.test.js +106 -307
  104. package/server/lib/provider-interface.js +99 -153
  105. package/server/lib/provider-interface.test.js +228 -394
  106. package/server/lib/provider-queue.js +164 -158
  107. package/server/lib/provider-queue.test.js +186 -315
  108. package/server/lib/router-config.js +99 -221
  109. package/server/lib/router-config.test.js +83 -237
  110. package/server/lib/router-setup-command.js +94 -419
  111. package/server/lib/router-setup-command.test.js +96 -375
  112. package/server/lib/router-status-api.js +93 -0
  113. package/server/lib/router-status-api.test.js +270 -0
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Box, Text, useApp, useInput } from 'ink';
3
- import { useState, useCallback } from 'react';
3
+ import { useState, useCallback, useEffect } from 'react';
4
4
  // Layout components
5
5
  import { Shell } from './components/layout/Shell.js';
6
6
  import { Sidebar, SidebarItem } from './components/layout/Sidebar.js';
@@ -20,6 +20,8 @@ import RouterPane from './components/RouterPane.js';
20
20
  import { UsagePane } from './components/UsagePane.js';
21
21
  import { SettingsPanel } from './components/SettingsPanel.js';
22
22
  import { AgentRegistryPane } from './components/AgentRegistryPane.js';
23
+ import { BugsPane } from './components/BugsPane.js';
24
+ import { UpdateBanner } from './components/UpdateBanner.js';
23
25
  // Utility components
24
26
  import { CommandPalette } from './components/CommandPalette.js';
25
27
  import { KeyboardHelp } from './components/KeyboardHelp.js';
@@ -36,6 +38,7 @@ const navItems = [
36
38
  { key: 'github', label: 'GitHub', icon: '🐙', shortcut: '7' },
37
39
  { key: 'health', label: 'Health', icon: '💚', shortcut: '8' },
38
40
  { key: 'router', label: 'Router', icon: '🔀', shortcut: '9' },
41
+ { key: 'bugs', label: 'Bugs', icon: '🐛', shortcut: 'b' },
39
42
  { key: 'settings', label: 'Settings', icon: '⚙️', shortcut: '0' },
40
43
  ];
41
44
  // Sample data for development
@@ -107,6 +110,7 @@ const commands = [
107
110
  { id: 'view:agents', name: 'Go to Agents', description: 'View agents', shortcut: '4', category: 'Navigation' },
108
111
  { id: 'view:logs', name: 'Go to Logs', description: 'View logs', shortcut: '6', category: 'Navigation' },
109
112
  { id: 'view:router', name: 'Go to Router', description: 'View model router', shortcut: '9', category: 'Navigation' },
113
+ { id: 'view:bugs', name: 'Go to Bugs', description: 'View and submit bugs', shortcut: 'b', category: 'Navigation' },
110
114
  { id: 'cmd:run-tests', name: 'Run Tests', description: 'Run test suite', category: 'Commands' },
111
115
  { id: 'cmd:build', name: 'Build Phase', description: 'Build current phase', category: 'Commands' },
112
116
  ];
@@ -131,6 +135,24 @@ export function App({ isTTY = true }) {
131
135
  // Data state
132
136
  const [selectedProject, setSelectedProject] = useState(null);
133
137
  const [connectionState] = useState('connected');
138
+ // Update banner state
139
+ const [updateInfo, setUpdateInfo] = useState(null);
140
+ const [updateDismissed, setUpdateDismissed] = useState(false);
141
+ // Check for updates on mount
142
+ useEffect(() => {
143
+ async function checkUpdates() {
144
+ try {
145
+ // @ts-ignore - version-checker is a JS module
146
+ const { checkForUpdates } = await import('../server/lib/version-checker.js');
147
+ const info = await checkForUpdates();
148
+ setUpdateInfo(info);
149
+ }
150
+ catch {
151
+ // Silently fail if version checker not available
152
+ }
153
+ }
154
+ checkUpdates();
155
+ }, []);
134
156
  // Keyboard handling
135
157
  useInput((input, key) => {
136
158
  // Global shortcuts
@@ -180,6 +202,8 @@ export function App({ isTTY = true }) {
180
202
  setActiveView('health');
181
203
  if (input === '9')
182
204
  setActiveView('router');
205
+ if (input === 'b')
206
+ setActiveView('bugs');
183
207
  if (input === '0')
184
208
  setActiveView('settings');
185
209
  // Tab cycles through views
@@ -229,6 +253,8 @@ export function App({ isTTY = true }) {
229
253
  return (_jsxs(Box, { flexDirection: "row", flexGrow: 1, children: [_jsx(Box, { width: "50%", flexDirection: "column", children: _jsx(HealthPane, {}) }), _jsx(Box, { width: "50%", flexDirection: "column", marginLeft: 1, children: _jsx(ServicesPane, { services: sampleServices, isActive: true }) })] }));
230
254
  case 'router':
231
255
  return (_jsxs(Box, { flexDirection: "row", flexGrow: 1, children: [_jsx(Box, { width: "60%", flexDirection: "column", children: _jsx(RouterPane, {}) }), _jsx(Box, { width: "40%", flexDirection: "column", marginLeft: 1, children: _jsx(UsagePane, {}) })] }));
256
+ case 'bugs':
257
+ return _jsx(BugsPane, { isActive: true, isTTY: isTTY });
232
258
  case 'settings':
233
259
  return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(SettingsPanel, { config: sampleConfig }), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: "gray", padding: 1, children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: "cyan", children: "Quick Links" }), _jsx(Text, { color: "gray", children: "[U] Usage [Q] Quality [D] Docs [W] Workspace [A] Audit" })] }) })] }));
234
260
  default:
@@ -241,5 +267,5 @@ export function App({ isTTY = true }) {
241
267
  const renderHeader = () => (_jsx(Header, { title: "TLC Dashboard", subtitle: viewTitle, status: connectionState === 'connected' ? 'online' : 'offline', actions: _jsxs(Box, { children: [_jsx(ConnectionStatus, { state: connectionState }), _jsx(Text, { color: "gray", children: " | " }), _jsx(Text, { dimColor: true, children: "v1.2.24" })] }) }));
242
268
  // Render footer/status bar
243
269
  const renderFooter = () => (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { dimColor: true, children: "Tab: cycle | 1-0: jump | Ctrl+B: sidebar | Ctrl+K: commands | ?: help | Ctrl+Q: quit" }), _jsx(StatusBar, {})] }));
244
- return (_jsxs(Box, { flexDirection: "column", width: "100%", height: "100%", children: [_jsx(Shell, { header: renderHeader(), footer: renderFooter(), sidebar: showSidebar ? renderSidebar() : undefined, showSidebar: showSidebar, sidebarWidth: 20, children: _jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { borderStyle: "single", borderColor: "cyan", borderBottom: true, borderTop: false, borderLeft: false, borderRight: false, paddingX: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: viewTitle }), selectedProject && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: " \u203A " }), _jsx(Text, { children: selectedProject.name })] }))] }), _jsx(Box, { flexGrow: 1, children: renderMainContent() })] }) }), showCommandPalette && (_jsx(Box, { position: "absolute", width: "60%", height: "50%", marginLeft: 10, marginTop: 5, borderStyle: "double", borderColor: "cyan", flexDirection: "column", children: _jsx(CommandPalette, { commands: commands, onSelect: handleCommandSelect, onClose: () => setShowCommandPalette(false) }) })), showHelp && (_jsx(Box, { position: "absolute", width: "70%", height: "80%", marginLeft: 8, marginTop: 3, borderStyle: "double", borderColor: "yellow", flexDirection: "column", children: _jsx(KeyboardHelp, { shortcuts: shortcuts, onClose: () => setShowHelp(false) }) }))] }));
270
+ return (_jsxs(Box, { flexDirection: "column", width: "100%", height: "100%", children: [_jsxs(Shell, { header: renderHeader(), footer: renderFooter(), sidebar: showSidebar ? renderSidebar() : undefined, showSidebar: showSidebar, sidebarWidth: 20, children: [updateInfo && !updateDismissed && (_jsx(UpdateBanner, { current: updateInfo.current, latest: updateInfo.latest, updateAvailable: updateInfo.updateAvailable, changelog: updateInfo.changelog, dismissable: true, onDismiss: () => setUpdateDismissed(true), compact: false, isActive: !showCommandPalette && !showHelp })), _jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { borderStyle: "single", borderColor: "cyan", borderBottom: true, borderTop: false, borderLeft: false, borderRight: false, paddingX: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: viewTitle }), selectedProject && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: " \u203A " }), _jsx(Text, { children: selectedProject.name })] }))] }), _jsx(Box, { flexGrow: 1, children: renderMainContent() })] })] }), showCommandPalette && (_jsx(Box, { position: "absolute", width: "60%", height: "50%", marginLeft: 10, marginTop: 5, borderStyle: "double", borderColor: "cyan", flexDirection: "column", children: _jsx(CommandPalette, { commands: commands, onSelect: handleCommandSelect, onClose: () => setShowCommandPalette(false) }) })), showHelp && (_jsx(Box, { position: "absolute", width: "70%", height: "80%", marginLeft: 8, marginTop: 3, borderStyle: "double", borderColor: "yellow", flexDirection: "column", children: _jsx(KeyboardHelp, { shortcuts: shortcuts, onClose: () => setShowHelp(false) }) }))] }));
245
271
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Health diagnostics module for TLC Dashboard
3
+ * Provides system health checks with self-repair suggestions
4
+ */
5
+ export interface DiagnosticCheck {
6
+ name: string;
7
+ status: 'ok' | 'warning' | 'error' | 'unknown';
8
+ message: string;
9
+ fix: string | null;
10
+ }
11
+ export interface DiagnosticsResult {
12
+ overall: 'healthy' | 'degraded' | 'unhealthy';
13
+ checks: DiagnosticCheck[];
14
+ }
15
+ /**
16
+ * Checks if .tlc.json configuration file exists
17
+ */
18
+ export declare function checkConfig(projectDir: string): Promise<DiagnosticCheck>;
19
+ /**
20
+ * Checks if required project files exist
21
+ */
22
+ export declare function checkRequiredFiles(projectDir: string): Promise<DiagnosticCheck>;
23
+ /**
24
+ * Runs all diagnostic checks and determines overall health status
25
+ */
26
+ export declare function runDiagnostics(projectDir: string): Promise<DiagnosticsResult>;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Health diagnostics module for TLC Dashboard
3
+ * Provides system health checks with self-repair suggestions
4
+ */
5
+ import * as fs from 'fs/promises';
6
+ import * as path from 'path';
7
+ /**
8
+ * Checks if .tlc.json configuration file exists
9
+ */
10
+ export async function checkConfig(projectDir) {
11
+ const configPath = path.join(projectDir, '.tlc.json');
12
+ try {
13
+ await fs.access(configPath);
14
+ return {
15
+ name: 'TLC Configuration',
16
+ status: 'ok',
17
+ message: 'Config found',
18
+ fix: null,
19
+ };
20
+ }
21
+ catch {
22
+ return {
23
+ name: 'TLC Configuration',
24
+ status: 'warning',
25
+ message: 'No .tlc.json found',
26
+ fix: 'Run: tlc init',
27
+ };
28
+ }
29
+ }
30
+ /**
31
+ * Checks if required project files exist
32
+ */
33
+ export async function checkRequiredFiles(projectDir) {
34
+ const required = ['package.json', '.planning/ROADMAP.md'];
35
+ const missing = [];
36
+ for (const file of required) {
37
+ const filePath = path.join(projectDir, file);
38
+ try {
39
+ await fs.access(filePath);
40
+ }
41
+ catch {
42
+ missing.push(file);
43
+ }
44
+ }
45
+ if (missing.length === 0) {
46
+ return {
47
+ name: 'Required Files',
48
+ status: 'ok',
49
+ message: 'All present',
50
+ fix: null,
51
+ };
52
+ }
53
+ return {
54
+ name: 'Required Files',
55
+ status: 'warning',
56
+ message: `Missing: ${missing.join(', ')}`,
57
+ fix: 'Run: tlc init',
58
+ };
59
+ }
60
+ /**
61
+ * Runs all diagnostic checks and determines overall health status
62
+ */
63
+ export async function runDiagnostics(projectDir) {
64
+ const checks = [];
65
+ // Run all checks
66
+ checks.push(await checkConfig(projectDir));
67
+ checks.push(await checkRequiredFiles(projectDir));
68
+ // Determine overall status
69
+ const hasErrors = checks.some(c => c.status === 'error');
70
+ const hasWarnings = checks.some(c => c.status === 'warning');
71
+ let overall;
72
+ if (hasErrors) {
73
+ overall = 'unhealthy';
74
+ }
75
+ else if (hasWarnings) {
76
+ overall = 'degraded';
77
+ }
78
+ else {
79
+ overall = 'healthy';
80
+ }
81
+ return {
82
+ overall,
83
+ checks,
84
+ };
85
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,126 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { vol } from 'memfs';
3
+ // Mock fs modules with memfs
4
+ vi.mock('fs', async () => {
5
+ const memfs = await import('memfs');
6
+ return memfs.fs;
7
+ });
8
+ vi.mock('fs/promises', async () => {
9
+ const memfs = await import('memfs');
10
+ return memfs.fs.promises;
11
+ });
12
+ // Import after mocks are set up
13
+ import { checkConfig, checkRequiredFiles, runDiagnostics, } from './health-diagnostics.js';
14
+ describe('health-diagnostics', () => {
15
+ const projectDir = '/test-project';
16
+ beforeEach(() => {
17
+ vol.reset();
18
+ vol.mkdirSync(projectDir, { recursive: true });
19
+ process.env.TLC_PROJECT_DIR = projectDir;
20
+ });
21
+ afterEach(() => {
22
+ vi.restoreAllMocks();
23
+ delete process.env.TLC_PROJECT_DIR;
24
+ });
25
+ describe('checkConfig', () => {
26
+ it('returns ok when .tlc.json exists', async () => {
27
+ vol.fromJSON({
28
+ [`${projectDir}/.tlc.json`]: '{}',
29
+ });
30
+ const result = await checkConfig(projectDir);
31
+ expect(result.name).toBe('TLC Configuration');
32
+ expect(result.status).toBe('ok');
33
+ expect(result.message).toBe('Config found');
34
+ expect(result.fix).toBeNull();
35
+ });
36
+ it('returns warning with fix when .tlc.json missing', async () => {
37
+ // No .tlc.json file
38
+ const result = await checkConfig(projectDir);
39
+ expect(result.name).toBe('TLC Configuration');
40
+ expect(result.status).toBe('warning');
41
+ expect(result.message).toBe('No .tlc.json found');
42
+ expect(result.fix).toBe('Run: tlc init');
43
+ });
44
+ });
45
+ describe('checkRequiredFiles', () => {
46
+ it('returns ok when all required files exist', async () => {
47
+ vol.fromJSON({
48
+ [`${projectDir}/package.json`]: '{}',
49
+ [`${projectDir}/.planning/ROADMAP.md`]: '# Roadmap',
50
+ });
51
+ const result = await checkRequiredFiles(projectDir);
52
+ expect(result.name).toBe('Required Files');
53
+ expect(result.status).toBe('ok');
54
+ expect(result.message).toBe('All present');
55
+ expect(result.fix).toBeNull();
56
+ });
57
+ it('returns warning when files are missing', async () => {
58
+ vol.fromJSON({
59
+ [`${projectDir}/package.json`]: '{}',
60
+ // Missing .planning/ROADMAP.md
61
+ });
62
+ const result = await checkRequiredFiles(projectDir);
63
+ expect(result.name).toBe('Required Files');
64
+ expect(result.status).toBe('warning');
65
+ expect(result.message).toContain('.planning/ROADMAP.md');
66
+ expect(result.fix).toBe('Run: tlc init');
67
+ });
68
+ it('lists all missing files in message', async () => {
69
+ // Both files missing
70
+ const result = await checkRequiredFiles(projectDir);
71
+ expect(result.status).toBe('warning');
72
+ expect(result.message).toContain('package.json');
73
+ expect(result.message).toContain('.planning/ROADMAP.md');
74
+ });
75
+ });
76
+ describe('runDiagnostics', () => {
77
+ it('returns all checks in result', async () => {
78
+ vol.fromJSON({
79
+ [`${projectDir}/.tlc.json`]: '{}',
80
+ [`${projectDir}/package.json`]: '{}',
81
+ [`${projectDir}/.planning/ROADMAP.md`]: '# Roadmap',
82
+ });
83
+ const result = await runDiagnostics(projectDir);
84
+ expect(result.checks).toBeDefined();
85
+ expect(Array.isArray(result.checks)).toBe(true);
86
+ expect(result.checks.length).toBeGreaterThan(0);
87
+ });
88
+ it('returns healthy when all checks ok', async () => {
89
+ vol.fromJSON({
90
+ [`${projectDir}/.tlc.json`]: '{}',
91
+ [`${projectDir}/package.json`]: '{}',
92
+ [`${projectDir}/.planning/ROADMAP.md`]: '# Roadmap',
93
+ });
94
+ const result = await runDiagnostics(projectDir);
95
+ expect(result.overall).toBe('healthy');
96
+ });
97
+ it('returns degraded when warnings but no errors', async () => {
98
+ vol.fromJSON({
99
+ // Missing .tlc.json (warning)
100
+ [`${projectDir}/package.json`]: '{}',
101
+ [`${projectDir}/.planning/ROADMAP.md`]: '# Roadmap',
102
+ });
103
+ const result = await runDiagnostics(projectDir);
104
+ expect(result.overall).toBe('degraded');
105
+ });
106
+ it('returns unhealthy when any check is error', async () => {
107
+ // Simulate an error condition by having checkConfig return error
108
+ // For now, test with missing project directory (causes error)
109
+ const result = await runDiagnostics('/nonexistent/path');
110
+ // Even with nonexistent path, we get warnings not errors from our current checks
111
+ // This test verifies the overall calculation logic
112
+ expect(['degraded', 'unhealthy']).toContain(result.overall);
113
+ });
114
+ it('includes diagnostic check names', async () => {
115
+ vol.fromJSON({
116
+ [`${projectDir}/.tlc.json`]: '{}',
117
+ [`${projectDir}/package.json`]: '{}',
118
+ [`${projectDir}/.planning/ROADMAP.md`]: '# Roadmap',
119
+ });
120
+ const result = await runDiagnostics(projectDir);
121
+ const checkNames = result.checks.map(c => c.name);
122
+ expect(checkNames).toContain('TLC Configuration');
123
+ expect(checkNames).toContain('Required Files');
124
+ });
125
+ });
126
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * API utilities for the TLC Dashboard
3
+ */
4
+ export { safeFetch, safePost, safePut, safeDelete, type FetchResult, type FetchError, type SafeFetchOptions, } from './safeFetch.js';
5
+ export { getNotes, updateNotes, type GetNotesResult, type UpdateNotesResult } from './notes-api.js';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * API utilities for the TLC Dashboard
3
+ */
4
+ export { safeFetch, safePost, safePut, safeDelete, } from './safeFetch.js';
5
+ export { getNotes, updateNotes } from './notes-api.js';
@@ -0,0 +1,18 @@
1
+ export interface GetNotesResult {
2
+ content: string;
3
+ lastModified: string | null;
4
+ error?: string;
5
+ }
6
+ export interface UpdateNotesResult {
7
+ success: boolean;
8
+ lastModified?: string;
9
+ error?: string;
10
+ }
11
+ /**
12
+ * Reads the content of PROJECT.md and returns it with the last modified timestamp.
13
+ */
14
+ export declare function getNotes(): Promise<GetNotesResult>;
15
+ /**
16
+ * Updates the content of PROJECT.md and returns the new last modified timestamp.
17
+ */
18
+ export declare function updateNotes(content: string): Promise<UpdateNotesResult>;
@@ -0,0 +1,68 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ /**
4
+ * Gets the project directory path.
5
+ * Uses TLC_PROJECT_DIR env var if set, otherwise uses current working directory.
6
+ */
7
+ function getProjectDir() {
8
+ return process.env.TLC_PROJECT_DIR || process.cwd();
9
+ }
10
+ /**
11
+ * Gets the path to PROJECT.md file.
12
+ */
13
+ function getProjectMdPath() {
14
+ return path.join(getProjectDir(), 'PROJECT.md');
15
+ }
16
+ /**
17
+ * Reads the content of PROJECT.md and returns it with the last modified timestamp.
18
+ */
19
+ export async function getNotes() {
20
+ const filePath = getProjectMdPath();
21
+ try {
22
+ const content = await fs.readFile(filePath, 'utf-8');
23
+ const stats = await fs.stat(filePath);
24
+ const lastModified = stats.mtime.toISOString();
25
+ return {
26
+ content,
27
+ lastModified,
28
+ };
29
+ }
30
+ catch (error) {
31
+ const nodeError = error;
32
+ // File not found - return empty content without error
33
+ if (nodeError.code === 'ENOENT') {
34
+ return {
35
+ content: '',
36
+ lastModified: null,
37
+ };
38
+ }
39
+ // Other errors - return empty content with error message
40
+ return {
41
+ content: '',
42
+ lastModified: null,
43
+ error: nodeError.message,
44
+ };
45
+ }
46
+ }
47
+ /**
48
+ * Updates the content of PROJECT.md and returns the new last modified timestamp.
49
+ */
50
+ export async function updateNotes(content) {
51
+ const filePath = getProjectMdPath();
52
+ try {
53
+ await fs.writeFile(filePath, content, 'utf-8');
54
+ const stats = await fs.stat(filePath);
55
+ const lastModified = stats.mtime.toISOString();
56
+ return {
57
+ success: true,
58
+ lastModified,
59
+ };
60
+ }
61
+ catch (error) {
62
+ const nodeError = error;
63
+ return {
64
+ success: false,
65
+ error: nodeError.message,
66
+ };
67
+ }
68
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,113 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { vol } from 'memfs';
3
+ // Mock fs modules with memfs
4
+ vi.mock('fs', async () => {
5
+ const memfs = await import('memfs');
6
+ return memfs.fs;
7
+ });
8
+ vi.mock('fs/promises', async () => {
9
+ const memfs = await import('memfs');
10
+ return memfs.fs.promises;
11
+ });
12
+ // Import after mocks are set up
13
+ import { getNotes, updateNotes } from './notes-api.js';
14
+ describe('notes-api', () => {
15
+ const projectDir = '/test-project';
16
+ const projectMdPath = `${projectDir}/PROJECT.md`;
17
+ const mockContent = '# Project Name\n\nThis is a test project.';
18
+ beforeEach(() => {
19
+ vol.reset();
20
+ // Set up project directory
21
+ vol.mkdirSync(projectDir, { recursive: true });
22
+ // Set the project directory for tests
23
+ process.env.TLC_PROJECT_DIR = projectDir;
24
+ });
25
+ afterEach(() => {
26
+ vi.restoreAllMocks();
27
+ delete process.env.TLC_PROJECT_DIR;
28
+ });
29
+ describe('getNotes', () => {
30
+ it('returns content from PROJECT.md', async () => {
31
+ vol.fromJSON({
32
+ [projectMdPath]: mockContent,
33
+ });
34
+ const result = await getNotes();
35
+ expect(result.content).toBe(mockContent);
36
+ });
37
+ it('returns empty content when file missing', async () => {
38
+ // No PROJECT.md file created
39
+ const result = await getNotes();
40
+ expect(result.content).toBe('');
41
+ expect(result.lastModified).toBeNull();
42
+ });
43
+ it('returns lastModified timestamp', async () => {
44
+ vol.fromJSON({
45
+ [projectMdPath]: mockContent,
46
+ });
47
+ const result = await getNotes();
48
+ expect(result.lastModified).toBeDefined();
49
+ expect(result.lastModified).not.toBeNull();
50
+ // Should be an ISO timestamp
51
+ expect(result.lastModified).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
52
+ });
53
+ it('handles read errors gracefully', async () => {
54
+ // Create a directory with the same name as PROJECT.md to cause read error
55
+ vol.mkdirSync(projectMdPath, { recursive: true });
56
+ const result = await getNotes();
57
+ expect(result.content).toBe('');
58
+ expect(result.lastModified).toBeNull();
59
+ expect(result.error).toBeDefined();
60
+ });
61
+ it('uses custom project directory from env', async () => {
62
+ const customDir = '/custom/path';
63
+ const customPath = `${customDir}/PROJECT.md`;
64
+ vol.mkdirSync(customDir, { recursive: true });
65
+ vol.fromJSON({
66
+ [customPath]: '# Custom Project',
67
+ });
68
+ process.env.TLC_PROJECT_DIR = customDir;
69
+ const result = await getNotes();
70
+ expect(result.content).toBe('# Custom Project');
71
+ });
72
+ });
73
+ describe('updateNotes', () => {
74
+ it('saves content to PROJECT.md', async () => {
75
+ const newContent = '# Updated Project\n\nNew content.';
76
+ const result = await updateNotes(newContent);
77
+ expect(result.success).toBe(true);
78
+ const savedContent = vol.readFileSync(projectMdPath, 'utf-8');
79
+ expect(savedContent).toBe(newContent);
80
+ });
81
+ it('creates file if missing', async () => {
82
+ const newContent = '# New Project';
83
+ // File doesn't exist yet
84
+ const result = await updateNotes(newContent);
85
+ expect(result.success).toBe(true);
86
+ const savedContent = vol.readFileSync(projectMdPath, 'utf-8');
87
+ expect(savedContent).toBe(newContent);
88
+ });
89
+ it('returns new lastModified', async () => {
90
+ const newContent = '# Updated';
91
+ const result = await updateNotes(newContent);
92
+ expect(result.lastModified).toBeDefined();
93
+ // Should be an ISO timestamp
94
+ expect(result.lastModified).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
95
+ });
96
+ it('handles write errors gracefully', async () => {
97
+ // Try to write to a path that doesn't exist and can't be created
98
+ process.env.TLC_PROJECT_DIR = '/nonexistent/deeply/nested/path';
99
+ const result = await updateNotes('content');
100
+ expect(result.success).toBe(false);
101
+ expect(result.error).toBeDefined();
102
+ });
103
+ it('uses custom project directory from env', async () => {
104
+ const customDir = '/custom/path';
105
+ const customPath = `${customDir}/PROJECT.md`;
106
+ vol.mkdirSync(customDir, { recursive: true });
107
+ process.env.TLC_PROJECT_DIR = customDir;
108
+ await updateNotes('# Custom content');
109
+ const savedContent = vol.readFileSync(customPath, 'utf-8');
110
+ expect(savedContent).toBe('# Custom content');
111
+ });
112
+ });
113
+ });
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Self-healing error boundary utilities for fetch operations.
3
+ * Provides consistent error handling, timeout support, and typed responses.
4
+ */
5
+ export interface FetchResult<T> {
6
+ data: T | null;
7
+ error: FetchError | null;
8
+ status: 'success' | 'error';
9
+ }
10
+ export interface FetchError {
11
+ type: 'http' | 'network' | 'timeout' | 'parse' | 'unknown';
12
+ code?: number;
13
+ message: string;
14
+ original?: Error;
15
+ }
16
+ export interface SafeFetchOptions extends RequestInit {
17
+ timeout?: number;
18
+ }
19
+ /**
20
+ * Safe fetch wrapper that never throws.
21
+ * Always returns a FetchResult with either data or error.
22
+ *
23
+ * @param url - The URL to fetch
24
+ * @param options - Fetch options plus optional timeout (default 10s)
25
+ * @returns Promise<FetchResult<T>> - Always resolves, never rejects
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * const { data, error } = await safeFetch<RouterData>('/api/router');
30
+ * if (error) {
31
+ * // Handle error - render error state
32
+ * return <ErrorState error={error} onRetry={() => refresh()} />;
33
+ * }
34
+ * // Use data safely
35
+ * ```
36
+ */
37
+ export declare function safeFetch<T>(url: string, options?: SafeFetchOptions): Promise<FetchResult<T>>;
38
+ /**
39
+ * POST helper with JSON body
40
+ */
41
+ export declare function safePost<T, B = unknown>(url: string, body: B, options?: SafeFetchOptions): Promise<FetchResult<T>>;
42
+ /**
43
+ * PUT helper with JSON body
44
+ */
45
+ export declare function safePut<T, B = unknown>(url: string, body: B, options?: SafeFetchOptions): Promise<FetchResult<T>>;
46
+ /**
47
+ * DELETE helper
48
+ */
49
+ export declare function safeDelete<T>(url: string, options?: SafeFetchOptions): Promise<FetchResult<T>>;
50
+ export default safeFetch;