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.
- package/dashboard/dist/App.js +28 -2
- package/dashboard/dist/api/health-diagnostics.d.ts +26 -0
- package/dashboard/dist/api/health-diagnostics.js +85 -0
- package/dashboard/dist/api/health-diagnostics.test.d.ts +1 -0
- package/dashboard/dist/api/health-diagnostics.test.js +126 -0
- package/dashboard/dist/api/index.d.ts +5 -0
- package/dashboard/dist/api/index.js +5 -0
- package/dashboard/dist/api/notes-api.d.ts +18 -0
- package/dashboard/dist/api/notes-api.js +68 -0
- package/dashboard/dist/api/notes-api.test.d.ts +1 -0
- package/dashboard/dist/api/notes-api.test.js +113 -0
- package/dashboard/dist/api/safeFetch.d.ts +50 -0
- package/dashboard/dist/api/safeFetch.js +135 -0
- package/dashboard/dist/api/safeFetch.test.d.ts +1 -0
- package/dashboard/dist/api/safeFetch.test.js +215 -0
- package/dashboard/dist/api/tasks-api.d.ts +32 -0
- package/dashboard/dist/api/tasks-api.js +98 -0
- package/dashboard/dist/api/tasks-api.test.d.ts +1 -0
- package/dashboard/dist/api/tasks-api.test.js +383 -0
- package/dashboard/dist/components/BugsPane.d.ts +20 -0
- package/dashboard/dist/components/BugsPane.js +210 -0
- package/dashboard/dist/components/BugsPane.test.d.ts +1 -0
- package/dashboard/dist/components/BugsPane.test.js +256 -0
- package/dashboard/dist/components/HealthPane.d.ts +3 -1
- package/dashboard/dist/components/HealthPane.js +44 -6
- package/dashboard/dist/components/HealthPane.test.js +105 -2
- package/dashboard/dist/components/RouterPane.d.ts +4 -3
- package/dashboard/dist/components/RouterPane.js +60 -57
- package/dashboard/dist/components/RouterPane.test.js +150 -96
- package/dashboard/dist/components/UpdateBanner.d.ts +26 -0
- package/dashboard/dist/components/UpdateBanner.js +30 -0
- package/dashboard/dist/components/UpdateBanner.test.d.ts +1 -0
- package/dashboard/dist/components/UpdateBanner.test.js +96 -0
- package/dashboard/dist/components/accessibility.test.d.ts +1 -0
- package/dashboard/dist/components/accessibility.test.js +116 -0
- package/dashboard/dist/components/layout/MobileNav.d.ts +16 -0
- package/dashboard/dist/components/layout/MobileNav.js +31 -0
- package/dashboard/dist/components/layout/MobileNav.test.d.ts +1 -0
- package/dashboard/dist/components/layout/MobileNav.test.js +111 -0
- package/dashboard/dist/components/performance.test.d.ts +1 -0
- package/dashboard/dist/components/performance.test.js +114 -0
- package/dashboard/dist/components/responsive.test.d.ts +1 -0
- package/dashboard/dist/components/responsive.test.js +114 -0
- package/dashboard/dist/components/ui/Dropdown.d.ts +22 -0
- package/dashboard/dist/components/ui/Dropdown.js +109 -0
- package/dashboard/dist/components/ui/Dropdown.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Dropdown.test.js +105 -0
- package/dashboard/dist/components/ui/EmptyState.d.ts +14 -0
- package/dashboard/dist/components/ui/EmptyState.js +58 -0
- package/dashboard/dist/components/ui/EmptyState.test.d.ts +1 -0
- package/dashboard/dist/components/ui/EmptyState.test.js +97 -0
- package/dashboard/dist/components/ui/ErrorState.d.ts +17 -0
- package/dashboard/dist/components/ui/ErrorState.js +80 -0
- package/dashboard/dist/components/ui/ErrorState.test.d.ts +1 -0
- package/dashboard/dist/components/ui/ErrorState.test.js +166 -0
- package/dashboard/dist/components/ui/Modal.d.ts +13 -0
- package/dashboard/dist/components/ui/Modal.js +25 -0
- package/dashboard/dist/components/ui/Modal.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Modal.test.js +91 -0
- package/dashboard/dist/components/ui/Skeleton.d.ts +32 -0
- package/dashboard/dist/components/ui/Skeleton.js +48 -0
- package/dashboard/dist/components/ui/Skeleton.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Skeleton.test.js +125 -0
- package/dashboard/dist/components/ui/Toast.d.ts +32 -0
- package/dashboard/dist/components/ui/Toast.js +21 -0
- package/dashboard/dist/components/ui/Toast.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Toast.test.js +118 -0
- package/dashboard/dist/hooks/useTheme.d.ts +37 -0
- package/dashboard/dist/hooks/useTheme.js +96 -0
- package/dashboard/dist/hooks/useTheme.test.d.ts +1 -0
- package/dashboard/dist/hooks/useTheme.test.js +94 -0
- package/dashboard/dist/hooks/useWebSocket.d.ts +17 -0
- package/dashboard/dist/hooks/useWebSocket.js +100 -0
- package/dashboard/dist/hooks/useWebSocket.test.d.ts +1 -0
- package/dashboard/dist/hooks/useWebSocket.test.js +115 -0
- package/dashboard/dist/stores/projectStore.d.ts +44 -0
- package/dashboard/dist/stores/projectStore.js +76 -0
- package/dashboard/dist/stores/projectStore.test.d.ts +1 -0
- package/dashboard/dist/stores/projectStore.test.js +114 -0
- package/dashboard/dist/stores/uiStore.d.ts +29 -0
- package/dashboard/dist/stores/uiStore.js +72 -0
- package/dashboard/dist/stores/uiStore.test.d.ts +1 -0
- package/dashboard/dist/stores/uiStore.test.js +93 -0
- package/dashboard/package.json +6 -3
- package/docker-compose.dev.yml +6 -1
- package/package.json +1 -1
- package/server/dashboard/index.html +1545 -791
- package/server/index.js +64 -0
- package/server/lib/api-provider.js +104 -186
- package/server/lib/api-provider.test.js +238 -336
- package/server/lib/cli-detector.js +90 -166
- package/server/lib/cli-detector.test.js +114 -269
- package/server/lib/cli-provider.js +142 -212
- package/server/lib/cli-provider.test.js +196 -349
- package/server/lib/debug.test.js +1 -1
- package/server/lib/devserver-router-api.js +54 -249
- package/server/lib/devserver-router-api.test.js +126 -426
- package/server/lib/introspect.js +309 -0
- package/server/lib/introspect.test.js +286 -0
- package/server/lib/model-router.js +107 -245
- package/server/lib/model-router.test.js +122 -313
- package/server/lib/output-schemas.js +146 -269
- package/server/lib/output-schemas.test.js +106 -307
- package/server/lib/provider-interface.js +99 -153
- package/server/lib/provider-interface.test.js +228 -394
- package/server/lib/provider-queue.js +164 -158
- package/server/lib/provider-queue.test.js +186 -315
- package/server/lib/router-config.js +99 -221
- package/server/lib/router-config.test.js +83 -237
- package/server/lib/router-setup-command.js +94 -419
- package/server/lib/router-setup-command.test.js +96 -375
- package/server/lib/router-status-api.js +93 -0
- package/server/lib/router-status-api.test.js +270 -0
package/dashboard/dist/App.js
CHANGED
|
@@ -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: [
|
|
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,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;
|