tlc-claude-code 1.4.4 → 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/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/package.json +3 -0
- package/package.json +1 -1
- package/server/dashboard/index.html +205 -8
- 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
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render } from 'ink-testing-library';
|
|
3
|
+
import { BugsPane } from './BugsPane.js';
|
|
4
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
5
|
+
// Mock fetch for API calls
|
|
6
|
+
const mockFetch = vi.fn();
|
|
7
|
+
global.fetch = mockFetch;
|
|
8
|
+
// Sample bug data
|
|
9
|
+
const sampleBugs = [
|
|
10
|
+
{
|
|
11
|
+
id: 'BUG-001',
|
|
12
|
+
title: 'Login button not working',
|
|
13
|
+
description: 'Click does nothing',
|
|
14
|
+
status: 'open',
|
|
15
|
+
priority: 'high',
|
|
16
|
+
createdAt: '2024-01-15',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'BUG-002',
|
|
20
|
+
title: 'Dashboard loads slowly',
|
|
21
|
+
description: 'Takes 5+ seconds',
|
|
22
|
+
status: 'in_progress',
|
|
23
|
+
priority: 'medium',
|
|
24
|
+
assignee: 'alice',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'BUG-003',
|
|
28
|
+
title: 'Minor typo in footer',
|
|
29
|
+
description: 'Copyrght misspelled',
|
|
30
|
+
status: 'fixed',
|
|
31
|
+
priority: 'low',
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
describe('BugsPane', () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
vi.clearAllMocks();
|
|
37
|
+
// Default successful response
|
|
38
|
+
mockFetch.mockResolvedValue({
|
|
39
|
+
ok: true,
|
|
40
|
+
json: () => Promise.resolve(sampleBugs),
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
vi.restoreAllMocks();
|
|
45
|
+
});
|
|
46
|
+
describe('rendering', () => {
|
|
47
|
+
it('renders the bugs pane with form and list sections', async () => {
|
|
48
|
+
const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
|
|
49
|
+
// Wait for fetch to complete
|
|
50
|
+
await vi.waitFor(() => {
|
|
51
|
+
expect(mockFetch).toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
const frame = lastFrame();
|
|
54
|
+
expect(frame).toContain('Submit New Bug');
|
|
55
|
+
expect(frame).toContain('Bug List');
|
|
56
|
+
expect(frame).toContain('Filters');
|
|
57
|
+
});
|
|
58
|
+
it('shows loading state initially', () => {
|
|
59
|
+
// Don't resolve fetch immediately
|
|
60
|
+
mockFetch.mockImplementation(() => new Promise(() => { }));
|
|
61
|
+
const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
|
|
62
|
+
const frame = lastFrame();
|
|
63
|
+
expect(frame).toContain('Loading');
|
|
64
|
+
});
|
|
65
|
+
it('displays bugs after loading', async () => {
|
|
66
|
+
const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
|
|
67
|
+
await vi.waitFor(() => {
|
|
68
|
+
const frame = lastFrame();
|
|
69
|
+
expect(frame).toContain('BUG-001');
|
|
70
|
+
});
|
|
71
|
+
const frame = lastFrame();
|
|
72
|
+
expect(frame).toContain('BUG-002');
|
|
73
|
+
expect(frame).toContain('BUG-003');
|
|
74
|
+
});
|
|
75
|
+
it('shows empty state when no bugs exist', async () => {
|
|
76
|
+
mockFetch.mockResolvedValue({
|
|
77
|
+
ok: true,
|
|
78
|
+
json: () => Promise.resolve([]),
|
|
79
|
+
});
|
|
80
|
+
const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
|
|
81
|
+
await vi.waitFor(() => {
|
|
82
|
+
const frame = lastFrame();
|
|
83
|
+
expect(frame).toContain('No bugs found');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
it('shows error state on fetch failure', async () => {
|
|
87
|
+
mockFetch.mockResolvedValue({
|
|
88
|
+
ok: false,
|
|
89
|
+
status: 500,
|
|
90
|
+
statusText: 'Internal Server Error',
|
|
91
|
+
});
|
|
92
|
+
const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
|
|
93
|
+
await vi.waitFor(() => {
|
|
94
|
+
const frame = lastFrame();
|
|
95
|
+
expect(frame).toContain('Error');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe('form', () => {
|
|
100
|
+
it('renders form fields', async () => {
|
|
101
|
+
const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
|
|
102
|
+
await vi.waitFor(() => {
|
|
103
|
+
expect(mockFetch).toHaveBeenCalled();
|
|
104
|
+
});
|
|
105
|
+
const frame = lastFrame();
|
|
106
|
+
expect(frame).toContain('Title');
|
|
107
|
+
expect(frame).toContain('Description');
|
|
108
|
+
expect(frame).toContain('Severity');
|
|
109
|
+
});
|
|
110
|
+
it('renders severity options', async () => {
|
|
111
|
+
const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
|
|
112
|
+
await vi.waitFor(() => {
|
|
113
|
+
expect(mockFetch).toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
const frame = lastFrame();
|
|
116
|
+
// Severity options are shown (selected one in uppercase brackets)
|
|
117
|
+
expect(frame).toContain('low');
|
|
118
|
+
expect(frame).toContain('MEDIUM'); // Selected by default, shown in uppercase
|
|
119
|
+
expect(frame).toContain('high');
|
|
120
|
+
expect(frame).toContain('critical');
|
|
121
|
+
});
|
|
122
|
+
it('defaults to medium severity', async () => {
|
|
123
|
+
const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
|
|
124
|
+
await vi.waitFor(() => {
|
|
125
|
+
expect(mockFetch).toHaveBeenCalled();
|
|
126
|
+
});
|
|
127
|
+
const frame = lastFrame();
|
|
128
|
+
// Medium should be highlighted/selected (in uppercase)
|
|
129
|
+
expect(frame).toContain('MEDIUM');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
describe('filters', () => {
|
|
133
|
+
it('renders filter buttons', async () => {
|
|
134
|
+
const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
|
|
135
|
+
await vi.waitFor(() => {
|
|
136
|
+
expect(mockFetch).toHaveBeenCalled();
|
|
137
|
+
});
|
|
138
|
+
const frame = lastFrame();
|
|
139
|
+
expect(frame).toContain('All');
|
|
140
|
+
expect(frame).toContain('Open');
|
|
141
|
+
expect(frame).toContain('Closed');
|
|
142
|
+
});
|
|
143
|
+
it('shows bug count in filters', async () => {
|
|
144
|
+
const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
|
|
145
|
+
await vi.waitFor(() => {
|
|
146
|
+
const frame = lastFrame();
|
|
147
|
+
expect(frame).toContain('3 bugs');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
describe('bug display', () => {
|
|
152
|
+
it('shows bug severity badges', async () => {
|
|
153
|
+
const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
|
|
154
|
+
await vi.waitFor(() => {
|
|
155
|
+
const frame = lastFrame();
|
|
156
|
+
expect(frame).toContain('HIGH');
|
|
157
|
+
});
|
|
158
|
+
const frame = lastFrame();
|
|
159
|
+
expect(frame).toContain('MEDIUM');
|
|
160
|
+
expect(frame).toContain('LOW');
|
|
161
|
+
});
|
|
162
|
+
it('shows bug status', async () => {
|
|
163
|
+
const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
|
|
164
|
+
await vi.waitFor(() => {
|
|
165
|
+
const frame = lastFrame();
|
|
166
|
+
expect(frame).toContain('[open]');
|
|
167
|
+
});
|
|
168
|
+
const frame = lastFrame();
|
|
169
|
+
// Status may wrap across lines in narrow terminals, check for key parts
|
|
170
|
+
expect(frame).toContain('in_progress');
|
|
171
|
+
expect(frame).toContain('[fixed]');
|
|
172
|
+
});
|
|
173
|
+
it('shows bug creation date when available', async () => {
|
|
174
|
+
const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
|
|
175
|
+
await vi.waitFor(() => {
|
|
176
|
+
const frame = lastFrame();
|
|
177
|
+
expect(frame).toContain('2024-01-15');
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe('API integration', () => {
|
|
182
|
+
it('fetches bugs on mount', async () => {
|
|
183
|
+
render(_jsx(BugsPane, { isActive: false, isTTY: false, apiBaseUrl: "http://test:5000" }));
|
|
184
|
+
await vi.waitFor(() => {
|
|
185
|
+
expect(mockFetch).toHaveBeenCalledWith('http://test:5000/api/bugs');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
it('uses default API URL', async () => {
|
|
189
|
+
render(_jsx(BugsPane, { isActive: false, isTTY: false }));
|
|
190
|
+
await vi.waitFor(() => {
|
|
191
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:5001/api/bugs');
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
describe('navigation hints', () => {
|
|
196
|
+
it('shows keyboard navigation hints', async () => {
|
|
197
|
+
const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
|
|
198
|
+
await vi.waitFor(() => {
|
|
199
|
+
expect(mockFetch).toHaveBeenCalled();
|
|
200
|
+
});
|
|
201
|
+
const frame = lastFrame();
|
|
202
|
+
expect(frame).toContain('Tab');
|
|
203
|
+
expect(frame).toContain('refresh');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
describe('severity colors', () => {
|
|
207
|
+
it('applies correct colors to severity badges', async () => {
|
|
208
|
+
// Just verify the component renders without errors with different severities
|
|
209
|
+
const bugsWithSeverities = [
|
|
210
|
+
{ id: 'BUG-001', title: 'Critical bug', description: '', status: 'open', priority: 'critical' },
|
|
211
|
+
{ id: 'BUG-002', title: 'High bug', description: '', status: 'open', priority: 'high' },
|
|
212
|
+
{ id: 'BUG-003', title: 'Medium bug', description: '', status: 'open', priority: 'medium' },
|
|
213
|
+
{ id: 'BUG-004', title: 'Low bug', description: '', status: 'open', priority: 'low' },
|
|
214
|
+
];
|
|
215
|
+
mockFetch.mockResolvedValue({
|
|
216
|
+
ok: true,
|
|
217
|
+
json: () => Promise.resolve(bugsWithSeverities),
|
|
218
|
+
});
|
|
219
|
+
const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
|
|
220
|
+
await vi.waitFor(() => {
|
|
221
|
+
const frame = lastFrame();
|
|
222
|
+
expect(frame).toContain('CRITICAL');
|
|
223
|
+
expect(frame).toContain('HIGH');
|
|
224
|
+
expect(frame).toContain('MEDIUM');
|
|
225
|
+
expect(frame).toContain('LOW');
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
describe('status colors', () => {
|
|
230
|
+
it('shows all status types correctly', async () => {
|
|
231
|
+
const bugsWithStatuses = [
|
|
232
|
+
{ id: 'BUG-001', title: 'Open', description: '', status: 'open', priority: 'medium' },
|
|
233
|
+
{ id: 'BUG-002', title: 'In Progress', description: '', status: 'in_progress', priority: 'medium' },
|
|
234
|
+
{ id: 'BUG-003', title: 'Fixed', description: '', status: 'fixed', priority: 'medium' },
|
|
235
|
+
{ id: 'BUG-004', title: 'Verified', description: '', status: 'verified', priority: 'medium' },
|
|
236
|
+
{ id: 'BUG-005', title: 'Closed', description: '', status: 'closed', priority: 'medium' },
|
|
237
|
+
{ id: 'BUG-006', title: 'Wontfix', description: '', status: 'wontfix', priority: 'medium' },
|
|
238
|
+
];
|
|
239
|
+
mockFetch.mockResolvedValue({
|
|
240
|
+
ok: true,
|
|
241
|
+
json: () => Promise.resolve(bugsWithStatuses),
|
|
242
|
+
});
|
|
243
|
+
const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
|
|
244
|
+
await vi.waitFor(() => {
|
|
245
|
+
const frame = lastFrame();
|
|
246
|
+
expect(frame).toContain('[open]');
|
|
247
|
+
// Status strings may wrap across lines in terminal output
|
|
248
|
+
expect(frame).toContain('in_progress');
|
|
249
|
+
expect(frame).toContain('[fixed]');
|
|
250
|
+
expect(frame).toContain('[verified]');
|
|
251
|
+
expect(frame).toContain('[closed]');
|
|
252
|
+
expect(frame).toContain('[wontfix]');
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DiagnosticsResult } from '../api/health-diagnostics.js';
|
|
1
2
|
interface SecurityData {
|
|
2
3
|
total: number;
|
|
3
4
|
critical: number;
|
|
@@ -17,6 +18,7 @@ interface HealthData {
|
|
|
17
18
|
}
|
|
18
19
|
interface HealthPaneProps {
|
|
19
20
|
data?: HealthData;
|
|
21
|
+
diagnostics?: DiagnosticsResult;
|
|
20
22
|
}
|
|
21
|
-
export declare function HealthPane({ data }: HealthPaneProps): import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
export declare function HealthPane({ data, diagnostics }: HealthPaneProps): import("react/jsx-runtime").JSX.Element;
|
|
22
24
|
export {};
|
|
@@ -1,12 +1,46 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Returns the appropriate icon for a diagnostic check status
|
|
5
|
+
*/
|
|
6
|
+
function getStatusIcon(status) {
|
|
7
|
+
switch (status) {
|
|
8
|
+
case 'ok': return '[ok]';
|
|
9
|
+
case 'warning': return '[!]';
|
|
10
|
+
case 'error': return '[X]';
|
|
11
|
+
case 'unknown': return '[?]';
|
|
12
|
+
default: return '[?]';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Returns the color for a diagnostic check status
|
|
17
|
+
*/
|
|
18
|
+
function getStatusColor(status) {
|
|
19
|
+
switch (status) {
|
|
20
|
+
case 'ok': return 'green';
|
|
21
|
+
case 'warning': return 'yellow';
|
|
22
|
+
case 'error': return 'red';
|
|
23
|
+
case 'unknown': return 'gray';
|
|
24
|
+
default: return 'gray';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Renders the diagnostics section with health checks
|
|
29
|
+
*/
|
|
30
|
+
function DiagnosticsSection({ diagnostics }) {
|
|
31
|
+
const overallColor = diagnostics.overall === 'healthy' ? 'green' :
|
|
32
|
+
diagnostics.overall === 'degraded' ? 'yellow' : 'red';
|
|
33
|
+
return (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, dimColor: true, children: "System Diagnostics:" }), _jsx(Box, { children: _jsxs(Text, { color: overallColor, children: [" Status: ", diagnostics.overall] }) }), diagnostics.checks.map((check, index) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: _jsxs(Text, { color: getStatusColor(check.status), children: [' ', getStatusIcon(check.status), " ", check.name, ": ", check.message] }) }), check.fix && (_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [" Fix: ", check.fix] }) }))] }, index)))] }));
|
|
34
|
+
}
|
|
35
|
+
export function HealthPane({ data, diagnostics }) {
|
|
36
|
+
if (!data && !diagnostics) {
|
|
5
37
|
return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Project Health" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "No health data available." }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Run /tlc:security to audit." }) })] }));
|
|
6
38
|
}
|
|
7
|
-
const { security, outdated } = data;
|
|
8
39
|
// Determine security status color
|
|
9
40
|
const getSecurityColor = () => {
|
|
41
|
+
if (!data)
|
|
42
|
+
return 'gray';
|
|
43
|
+
const { security } = data;
|
|
10
44
|
if (security.critical > 0)
|
|
11
45
|
return 'red';
|
|
12
46
|
if (security.high > 0)
|
|
@@ -19,6 +53,9 @@ export function HealthPane({ data }) {
|
|
|
19
53
|
};
|
|
20
54
|
// Determine outdated status color
|
|
21
55
|
const getOutdatedColor = () => {
|
|
56
|
+
if (!data)
|
|
57
|
+
return 'gray';
|
|
58
|
+
const { outdated } = data;
|
|
22
59
|
if (outdated.major && outdated.major > 0)
|
|
23
60
|
return 'yellow';
|
|
24
61
|
if (outdated.total > 10)
|
|
@@ -27,7 +64,8 @@ export function HealthPane({ data }) {
|
|
|
27
64
|
return 'gray';
|
|
28
65
|
return 'green';
|
|
29
66
|
};
|
|
30
|
-
return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Project Health" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, dimColor: true, children: "Security:" }), security.total === 0 ? (_jsx(Box, { children: _jsx(Text, { color: "green", children: "
|
|
31
|
-
outdated.total > 0 ? '/tlc:outdated to update' :
|
|
32
|
-
'
|
|
67
|
+
return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Project Health" }), diagnostics && _jsx(DiagnosticsSection, { diagnostics: diagnostics }), data && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, dimColor: true, children: "Security:" }), data.security.total === 0 ? (_jsx(Box, { children: _jsx(Text, { color: "green", children: " [ok] No vulnerabilities" }) })) : (_jsxs(_Fragment, { children: [_jsx(Box, { children: _jsxs(Text, { color: getSecurityColor(), children: [data.security.total, " vulnerabilities"] }) }), data.security.critical > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "red", children: [" [!!] Critical: ", data.security.critical] }) })), data.security.high > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "red", children: [" [!] High: ", data.security.high] }) })), data.security.moderate > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "yellow", children: [" [~] Moderate: ", data.security.moderate] }) })), data.security.low > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "gray", children: [" [-] Low: ", data.security.low] }) }))] }))] })), data && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, dimColor: true, children: "Dependencies:" }), data.outdated.total === 0 ? (_jsx(Box, { children: _jsx(Text, { color: "green", children: " [ok] All up to date" }) })) : (_jsxs(_Fragment, { children: [_jsx(Box, { children: _jsxs(Text, { color: getOutdatedColor(), children: [data.outdated.total, " outdated"] }) }), data.outdated.major !== undefined && data.outdated.major > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "yellow", children: [" [!] Major: ", data.outdated.major] }) })), data.outdated.minor !== undefined && data.outdated.minor > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "gray", children: [" [~] Minor: ", data.outdated.minor] }) })), data.outdated.patch !== undefined && data.outdated.patch > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "gray", children: [" [-] Patch: ", data.outdated.patch] }) }))] }))] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: data && data.security.total > 0 ? '/tlc:security to fix' :
|
|
68
|
+
data && data.outdated.total > 0 ? '/tlc:outdated to update' :
|
|
69
|
+
diagnostics && diagnostics.overall !== 'healthy' ? 'Fix issues above' :
|
|
70
|
+
'Healthy!' }) })] }));
|
|
33
71
|
}
|
|
@@ -27,14 +27,14 @@ describe('HealthPane', () => {
|
|
|
27
27
|
const output = lastFrame();
|
|
28
28
|
expect(output).toContain('3'); // vulnerability count
|
|
29
29
|
});
|
|
30
|
-
it('shows
|
|
30
|
+
it('shows ok indicator when no vulnerabilities', () => {
|
|
31
31
|
const healthData = {
|
|
32
32
|
security: { total: 0, critical: 0, high: 0, moderate: 0, low: 0 },
|
|
33
33
|
outdated: { total: 0 },
|
|
34
34
|
};
|
|
35
35
|
const { lastFrame } = render(_jsx(HealthPane, { data: healthData }));
|
|
36
36
|
const output = lastFrame();
|
|
37
|
-
expect(output).toContain('
|
|
37
|
+
expect(output).toContain('[ok]');
|
|
38
38
|
});
|
|
39
39
|
it('shows warning for critical vulnerabilities', () => {
|
|
40
40
|
const healthData = {
|
|
@@ -64,4 +64,107 @@ describe('HealthPane', () => {
|
|
|
64
64
|
// Just verify it renders
|
|
65
65
|
expect(output).toContain('5');
|
|
66
66
|
});
|
|
67
|
+
describe('diagnostics', () => {
|
|
68
|
+
it('shows diagnostics section when provided', () => {
|
|
69
|
+
const diagnostics = {
|
|
70
|
+
overall: 'healthy',
|
|
71
|
+
checks: [
|
|
72
|
+
{ name: 'TLC Configuration', status: 'ok', message: 'Config found', fix: null },
|
|
73
|
+
{ name: 'Required Files', status: 'ok', message: 'All present', fix: null },
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
const { lastFrame } = render(_jsx(HealthPane, { diagnostics: diagnostics }));
|
|
77
|
+
const output = lastFrame();
|
|
78
|
+
expect(output).toContain('System Diagnostics');
|
|
79
|
+
expect(output).toContain('healthy');
|
|
80
|
+
});
|
|
81
|
+
it('shows check names and statuses', () => {
|
|
82
|
+
const diagnostics = {
|
|
83
|
+
overall: 'healthy',
|
|
84
|
+
checks: [
|
|
85
|
+
{ name: 'TLC Configuration', status: 'ok', message: 'Config found', fix: null },
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
const { lastFrame } = render(_jsx(HealthPane, { diagnostics: diagnostics }));
|
|
89
|
+
const output = lastFrame();
|
|
90
|
+
expect(output).toContain('TLC Configuration');
|
|
91
|
+
expect(output).toContain('[ok]');
|
|
92
|
+
expect(output).toContain('Config found');
|
|
93
|
+
});
|
|
94
|
+
it('shows fix suggestions for warnings', () => {
|
|
95
|
+
const diagnostics = {
|
|
96
|
+
overall: 'degraded',
|
|
97
|
+
checks: [
|
|
98
|
+
{ name: 'TLC Configuration', status: 'warning', message: 'No .tlc.json found', fix: 'Run: tlc init' },
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
const { lastFrame } = render(_jsx(HealthPane, { diagnostics: diagnostics }));
|
|
102
|
+
const output = lastFrame();
|
|
103
|
+
expect(output).toContain('[!]');
|
|
104
|
+
expect(output).toContain('No .tlc.json found');
|
|
105
|
+
expect(output).toContain('Run: tlc init');
|
|
106
|
+
});
|
|
107
|
+
it('shows degraded status with warning color', () => {
|
|
108
|
+
const diagnostics = {
|
|
109
|
+
overall: 'degraded',
|
|
110
|
+
checks: [
|
|
111
|
+
{ name: 'Test Check', status: 'warning', message: 'Issue found', fix: 'Fix it' },
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
const { lastFrame } = render(_jsx(HealthPane, { diagnostics: diagnostics }));
|
|
115
|
+
const output = lastFrame();
|
|
116
|
+
expect(output).toContain('degraded');
|
|
117
|
+
});
|
|
118
|
+
it('shows unhealthy status with error color', () => {
|
|
119
|
+
const diagnostics = {
|
|
120
|
+
overall: 'unhealthy',
|
|
121
|
+
checks: [
|
|
122
|
+
{ name: 'Critical Check', status: 'error', message: 'Critical error', fix: 'Fix now' },
|
|
123
|
+
],
|
|
124
|
+
};
|
|
125
|
+
const { lastFrame } = render(_jsx(HealthPane, { diagnostics: diagnostics }));
|
|
126
|
+
const output = lastFrame();
|
|
127
|
+
expect(output).toContain('unhealthy');
|
|
128
|
+
expect(output).toContain('[X]');
|
|
129
|
+
});
|
|
130
|
+
it('shows unknown status with question mark', () => {
|
|
131
|
+
const diagnostics = {
|
|
132
|
+
overall: 'degraded',
|
|
133
|
+
checks: [
|
|
134
|
+
{ name: 'Unknown Check', status: 'unknown', message: 'Cannot determine', fix: null },
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
const { lastFrame } = render(_jsx(HealthPane, { diagnostics: diagnostics }));
|
|
138
|
+
const output = lastFrame();
|
|
139
|
+
expect(output).toContain('[?]');
|
|
140
|
+
});
|
|
141
|
+
it('renders with both data and diagnostics', () => {
|
|
142
|
+
const healthData = {
|
|
143
|
+
security: { total: 0, critical: 0, high: 0, moderate: 0, low: 0 },
|
|
144
|
+
outdated: { total: 0 },
|
|
145
|
+
};
|
|
146
|
+
const diagnostics = {
|
|
147
|
+
overall: 'healthy',
|
|
148
|
+
checks: [
|
|
149
|
+
{ name: 'TLC Configuration', status: 'ok', message: 'Config found', fix: null },
|
|
150
|
+
],
|
|
151
|
+
};
|
|
152
|
+
const { lastFrame } = render(_jsx(HealthPane, { data: healthData, diagnostics: diagnostics }));
|
|
153
|
+
const output = lastFrame();
|
|
154
|
+
expect(output).toContain('System Diagnostics');
|
|
155
|
+
expect(output).toContain('Security');
|
|
156
|
+
expect(output).toContain('Dependencies');
|
|
157
|
+
});
|
|
158
|
+
it('shows fix issues hint when diagnostics not healthy', () => {
|
|
159
|
+
const diagnostics = {
|
|
160
|
+
overall: 'degraded',
|
|
161
|
+
checks: [
|
|
162
|
+
{ name: 'TLC Configuration', status: 'warning', message: 'No .tlc.json found', fix: 'Run: tlc init' },
|
|
163
|
+
],
|
|
164
|
+
};
|
|
165
|
+
const { lastFrame } = render(_jsx(HealthPane, { diagnostics: diagnostics }));
|
|
166
|
+
const output = lastFrame();
|
|
167
|
+
expect(output).toContain('Fix issues above');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
67
170
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
interface RouterPaneProps {
|
|
2
|
-
|
|
2
|
+
apiBaseUrl?: string;
|
|
3
|
+
refreshInterval?: number;
|
|
3
4
|
}
|
|
4
|
-
export
|
|
5
|
-
export
|
|
5
|
+
export declare function RouterPane({ apiBaseUrl, refreshInterval, }: RouterPaneProps): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
export default RouterPane;
|
|
@@ -1,65 +1,68 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
3
|
import { Box, Text } from 'ink';
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import Spinner from 'ink-spinner';
|
|
5
|
+
import { safeFetch } from '../api/safeFetch.js';
|
|
6
|
+
import { ErrorState } from './ui/ErrorState.js';
|
|
7
|
+
import { EmptyState } from './ui/EmptyState.js';
|
|
8
|
+
import { Skeleton } from './ui/Skeleton.js';
|
|
9
|
+
export function RouterPane({ apiBaseUrl = 'http://localhost:5001', refreshInterval = 30000, }) {
|
|
10
|
+
const [data, setData] = useState(null);
|
|
7
11
|
const [error, setError] = useState(null);
|
|
12
|
+
const [loadingState, setLoadingState] = useState('loading');
|
|
13
|
+
const fetchRouterData = useCallback(async () => {
|
|
14
|
+
const result = await safeFetch(`${apiBaseUrl}/api/router`);
|
|
15
|
+
if (result.error) {
|
|
16
|
+
setError(result.error);
|
|
17
|
+
setLoadingState('error');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (!result.data || Object.keys(result.data.providers || {}).length === 0) {
|
|
21
|
+
setData(result.data);
|
|
22
|
+
setLoadingState('empty');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
setData(result.data);
|
|
26
|
+
setError(null);
|
|
27
|
+
setLoadingState('success');
|
|
28
|
+
}, [apiBaseUrl]);
|
|
8
29
|
useEffect(() => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
setStatus(data);
|
|
17
|
-
setError(null);
|
|
18
|
-
}
|
|
19
|
-
catch (err) {
|
|
20
|
-
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
21
|
-
}
|
|
22
|
-
finally {
|
|
23
|
-
setLoading(false);
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
fetchStatus();
|
|
27
|
-
}, [apiUrl]);
|
|
28
|
-
if (loading) {
|
|
29
|
-
return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Model Router" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "Loading router status..." }) })] }));
|
|
30
|
+
fetchRouterData();
|
|
31
|
+
const interval = setInterval(fetchRouterData, refreshInterval);
|
|
32
|
+
return () => clearInterval(interval);
|
|
33
|
+
}, [fetchRouterData, refreshInterval]);
|
|
34
|
+
// Loading state with skeleton
|
|
35
|
+
if (loadingState === 'loading') {
|
|
36
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "Model Router" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Loading..." })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Skeleton.Text, { lines: 3, width: 30 }) })] }));
|
|
30
37
|
}
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
// Error state
|
|
39
|
+
if (loadingState === 'error' && error) {
|
|
40
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "Model Router" }), _jsx(Box, { marginTop: 1, children: _jsx(ErrorState, { error: error, onRetry: fetchRouterData }) })] }));
|
|
33
41
|
}
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
// Empty state
|
|
43
|
+
if (loadingState === 'empty' || !data) {
|
|
44
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "Model Router" }), _jsx(Box, { marginTop: 1, children: _jsx(EmptyState, { type: "router" }) })] }));
|
|
36
45
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const healthIndicator = provider.healthy !== false ? '●' : '○';
|
|
44
|
-
const healthColor = provider.healthy !== false ? 'green' : 'red';
|
|
45
|
-
const routingBadge = isLocal ? 'local' : 'devserver';
|
|
46
|
-
const badgeColor = isLocal ? 'green' : 'yellow';
|
|
47
|
-
return (_jsxs(Box, { children: [_jsxs(Text, { color: healthColor, children: [healthIndicator, " "] }), _jsx(Text, { bold: true, children: name.padEnd(10) }), provider.version && _jsxs(Text, { color: "gray", children: [" ", provider.version.padEnd(10)] }), _jsxs(Text, { color: badgeColor, children: ["[", routingBadge, "]"] }), provider.capabilities && provider.capabilities.length > 0 && (_jsxs(Text, { color: "gray", children: [" (", provider.capabilities.join(', '), ")"] }))] }));
|
|
48
|
-
}
|
|
49
|
-
function DevserverRow({ devserver }) {
|
|
50
|
-
if (!devserver.configured) {
|
|
51
|
-
return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "gray", children: "Not configured - run " }), _jsx(Text, { color: "cyan", children: "tlc router setup" })] }));
|
|
46
|
+
// Calculate total estimated cost
|
|
47
|
+
let totalCost = 0;
|
|
48
|
+
if (data.costEstimate) {
|
|
49
|
+
for (const cap of Object.values(data.costEstimate)) {
|
|
50
|
+
totalCost += cap.devserver || 0;
|
|
51
|
+
}
|
|
52
52
|
}
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
53
|
+
const costStr = totalCost > 0 ? `$${totalCost.toFixed(2)}` : '$0.00';
|
|
54
|
+
// Build routing display from capabilities
|
|
55
|
+
const routingEntries = [];
|
|
56
|
+
if (data.capabilities) {
|
|
57
|
+
for (const [capability, info] of Object.entries(data.capabilities)) {
|
|
58
|
+
const providers = info.providers.map((name) => {
|
|
59
|
+
const providerInfo = data.providers[name];
|
|
60
|
+
const location = providerInfo?.type === 'cli' && providerInfo?.detected ? 'local' : 'devserver';
|
|
61
|
+
return { name, location };
|
|
62
|
+
});
|
|
63
|
+
routingEntries.push({ capability, providers });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "Model Router" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Local CLIs" }), Object.entries(data.providers).map(([name, info]) => (_jsxs(Box, { children: [_jsxs(Text, { color: info.detected ? 'green' : 'gray', children: [info.detected ? '\u2713' : '\u25CB', " ", name] }), info.version && _jsxs(Text, { dimColor: true, children: [" v", info.version] }), _jsxs(Text, { dimColor: true, children: [" - ", info.detected ? 'available' : 'not found'] })] }, name))), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Devserver" }), _jsx(Text, { color: data.devserver.connected ? 'green' : 'red', children: data.devserver.connected ? '\u25CF Connected' : '\u25CF Disconnected' }), routingEntries.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Routing" }), routingEntries.map(({ capability, providers }) => (_jsxs(Box, { children: [_jsxs(Text, { children: [capability, ": "] }), providers.map((p, i) => (_jsxs(Text, { color: p.location === 'local' ? 'cyan' : 'yellow', children: [i > 0 ? ' \u2192 ' : '', p.name, " (", p.location, ")"] }, p.name)))] }, capability)))] })), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Estimated Cost" }), _jsxs(Text, { color: "yellow", children: [costStr, "/day"] })] }));
|
|
65
67
|
}
|
|
68
|
+
export default RouterPane;
|