tlc-claude-code 1.4.4 → 1.4.6

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 (72) 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/ui/EmptyState.d.ts +14 -0
  35. package/dashboard/dist/components/ui/EmptyState.js +58 -0
  36. package/dashboard/dist/components/ui/EmptyState.test.d.ts +1 -0
  37. package/dashboard/dist/components/ui/EmptyState.test.js +97 -0
  38. package/dashboard/dist/components/ui/ErrorState.d.ts +17 -0
  39. package/dashboard/dist/components/ui/ErrorState.js +80 -0
  40. package/dashboard/dist/components/ui/ErrorState.test.d.ts +1 -0
  41. package/dashboard/dist/components/ui/ErrorState.test.js +166 -0
  42. package/dashboard/package.json +3 -0
  43. package/package.json +4 -1
  44. package/server/dashboard/index.html +284 -13
  45. package/server/dashboard/login.html +262 -0
  46. package/server/index.js +304 -0
  47. package/server/lib/api-provider.js +104 -186
  48. package/server/lib/api-provider.test.js +238 -336
  49. package/server/lib/cli-detector.js +90 -166
  50. package/server/lib/cli-detector.test.js +114 -269
  51. package/server/lib/cli-provider.js +142 -212
  52. package/server/lib/cli-provider.test.js +196 -349
  53. package/server/lib/debug.test.js +3 -3
  54. package/server/lib/devserver-router-api.js +54 -249
  55. package/server/lib/devserver-router-api.test.js +126 -426
  56. package/server/lib/introspect.js +309 -0
  57. package/server/lib/introspect.test.js +286 -0
  58. package/server/lib/model-router.js +107 -245
  59. package/server/lib/model-router.test.js +122 -313
  60. package/server/lib/output-schemas.js +146 -269
  61. package/server/lib/output-schemas.test.js +106 -307
  62. package/server/lib/plan-parser.js +59 -16
  63. package/server/lib/provider-interface.js +99 -153
  64. package/server/lib/provider-interface.test.js +228 -394
  65. package/server/lib/provider-queue.js +164 -158
  66. package/server/lib/provider-queue.test.js +186 -315
  67. package/server/lib/router-config.js +99 -221
  68. package/server/lib/router-config.test.js +83 -237
  69. package/server/lib/router-setup-command.js +94 -419
  70. package/server/lib/router-setup-command.test.js +96 -375
  71. package/server/lib/router-status-api.js +93 -0
  72. package/server/lib/router-status-api.test.js +270 -0
@@ -0,0 +1,97 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, it, expect } from 'vitest';
3
+ import { render } from 'ink-testing-library';
4
+ import { EmptyState } from './EmptyState.js';
5
+ describe('EmptyState', () => {
6
+ describe('Type-specific defaults', () => {
7
+ it('renders tasks empty state with default messages', () => {
8
+ const { lastFrame } = render(_jsx(EmptyState, { type: "tasks" }));
9
+ const output = lastFrame() || '';
10
+ expect(output).toContain('No tasks yet');
11
+ expect(output).toContain('/tlc:plan');
12
+ });
13
+ it('renders bugs empty state', () => {
14
+ const { lastFrame } = render(_jsx(EmptyState, { type: "bugs" }));
15
+ const output = lastFrame() || '';
16
+ expect(output).toContain('No bugs reported');
17
+ expect(output).toContain('/tlc:bug');
18
+ });
19
+ it('renders agents empty state', () => {
20
+ const { lastFrame } = render(_jsx(EmptyState, { type: "agents" }));
21
+ const output = lastFrame() || '';
22
+ expect(output).toContain('No agents running');
23
+ expect(output).toContain('spawned');
24
+ });
25
+ it('renders logs empty state', () => {
26
+ const { lastFrame } = render(_jsx(EmptyState, { type: "logs" }));
27
+ const output = lastFrame() || '';
28
+ expect(output).toContain('No logs yet');
29
+ expect(output).toContain('activity');
30
+ });
31
+ it('renders projects empty state', () => {
32
+ const { lastFrame } = render(_jsx(EmptyState, { type: "projects" }));
33
+ const output = lastFrame() || '';
34
+ expect(output).toContain('No projects');
35
+ expect(output).toContain('tlc init');
36
+ });
37
+ it('renders health empty state', () => {
38
+ const { lastFrame } = render(_jsx(EmptyState, { type: "health" }));
39
+ const output = lastFrame() || '';
40
+ expect(output).toContain('No health data');
41
+ expect(output).toContain('/tlc:security');
42
+ });
43
+ it('renders router empty state', () => {
44
+ const { lastFrame } = render(_jsx(EmptyState, { type: "router" }));
45
+ const output = lastFrame() || '';
46
+ expect(output).toContain('No router configured');
47
+ expect(output).toContain('.tlc.json');
48
+ });
49
+ it('renders generic empty state as default', () => {
50
+ const { lastFrame } = render(_jsx(EmptyState, {}));
51
+ const output = lastFrame() || '';
52
+ expect(output).toContain('Nothing here');
53
+ expect(output).toContain('No items');
54
+ });
55
+ });
56
+ describe('Custom messages', () => {
57
+ it('uses custom title when provided', () => {
58
+ const { lastFrame } = render(_jsx(EmptyState, { type: "tasks", title: "Custom Title" }));
59
+ const output = lastFrame() || '';
60
+ expect(output).toContain('Custom Title');
61
+ expect(output).not.toContain('No tasks yet');
62
+ });
63
+ it('uses custom subtitle when provided', () => {
64
+ const { lastFrame } = render(_jsx(EmptyState, { type: "tasks", subtitle: "Custom subtitle" }));
65
+ const output = lastFrame() || '';
66
+ expect(output).toContain('Custom subtitle');
67
+ expect(output).not.toContain('/tlc:plan');
68
+ });
69
+ it('shows action hint when provided', () => {
70
+ const { lastFrame } = render(_jsx(EmptyState, { type: "generic", action: "Press Enter to add" }));
71
+ const output = lastFrame() || '';
72
+ expect(output).toContain('Press Enter to add');
73
+ });
74
+ });
75
+ describe('Compact mode', () => {
76
+ it('renders compact state with title only', () => {
77
+ const { lastFrame } = render(_jsx(EmptyState, { type: "tasks", compact: true }));
78
+ const output = lastFrame() || '';
79
+ expect(output).toContain('No tasks yet');
80
+ // Should be shorter than full version
81
+ expect(output.split('\n').length).toBeLessThan(5);
82
+ });
83
+ it('hides subtitle in compact mode', () => {
84
+ const { lastFrame } = render(_jsx(EmptyState, { type: "tasks", compact: true }));
85
+ const output = lastFrame() || '';
86
+ expect(output).not.toContain('/tlc:plan');
87
+ });
88
+ });
89
+ describe('Icons', () => {
90
+ it('shows an icon', () => {
91
+ const { lastFrame } = render(_jsx(EmptyState, { type: "tasks" }));
92
+ const output = lastFrame() || '';
93
+ // Should contain some icon character
94
+ expect(output.length).toBeGreaterThan(0);
95
+ });
96
+ });
97
+ });
@@ -0,0 +1,17 @@
1
+ import type { FetchError } from '../../api/safeFetch.js';
2
+ export interface ErrorInfo {
3
+ title: string;
4
+ message: string;
5
+ hint?: string;
6
+ }
7
+ export interface ErrorStateProps {
8
+ error: FetchError;
9
+ onRetry?: () => void;
10
+ compact?: boolean;
11
+ }
12
+ /**
13
+ * Renders a user-friendly error state with optional retry button.
14
+ * Maps technical errors to helpful messages with actionable hints.
15
+ */
16
+ export declare function ErrorState({ error, onRetry, compact }: ErrorStateProps): import("react/jsx-runtime").JSX.Element;
17
+ export default ErrorState;
@@ -0,0 +1,80 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ /**
4
+ * Maps error types to user-friendly messages with actionable hints
5
+ */
6
+ const ERROR_MESSAGES = {
7
+ 'http-404': {
8
+ title: 'Not Set Up',
9
+ message: 'This feature needs configuration.',
10
+ hint: 'Run tlc setup',
11
+ },
12
+ 'http-401': {
13
+ title: 'Not Authorized',
14
+ message: 'Authentication required.',
15
+ hint: 'Check your credentials',
16
+ },
17
+ 'http-403': {
18
+ title: 'Access Denied',
19
+ message: 'You do not have permission.',
20
+ },
21
+ 'http-500': {
22
+ title: 'Server Error',
23
+ message: 'Something went wrong on the server.',
24
+ },
25
+ 'http-502': {
26
+ title: 'Bad Gateway',
27
+ message: 'The server is unreachable.',
28
+ hint: 'Check if tlc server is running',
29
+ },
30
+ 'http-503': {
31
+ title: 'Service Unavailable',
32
+ message: 'The server is temporarily unavailable.',
33
+ },
34
+ network: {
35
+ title: 'Connection Lost',
36
+ message: 'Cannot reach the server.',
37
+ hint: 'Check if tlc server is running',
38
+ },
39
+ timeout: {
40
+ title: 'Request Timeout',
41
+ message: 'The server took too long to respond.',
42
+ hint: 'Try again or check server health',
43
+ },
44
+ parse: {
45
+ title: 'Invalid Response',
46
+ message: 'The server returned unexpected data.',
47
+ },
48
+ unknown: {
49
+ title: 'Unknown Error',
50
+ message: 'An unexpected error occurred.',
51
+ },
52
+ };
53
+ function getErrorInfo(error) {
54
+ // Check for specific HTTP error codes
55
+ if (error.type === 'http' && error.code) {
56
+ const key = `http-${error.code}`;
57
+ if (key in ERROR_MESSAGES) {
58
+ return ERROR_MESSAGES[key];
59
+ }
60
+ // Default HTTP error
61
+ return {
62
+ title: `HTTP Error ${error.code}`,
63
+ message: error.message,
64
+ };
65
+ }
66
+ // Check for other error types
67
+ return ERROR_MESSAGES[error.type] || ERROR_MESSAGES.unknown;
68
+ }
69
+ /**
70
+ * Renders a user-friendly error state with optional retry button.
71
+ * Maps technical errors to helpful messages with actionable hints.
72
+ */
73
+ export function ErrorState({ error, onRetry, compact = false }) {
74
+ const info = getErrorInfo(error);
75
+ if (compact) {
76
+ return (_jsxs(Box, { children: [_jsx(Text, { color: "red", children: info.title }), onRetry && _jsx(Text, { dimColor: true, children: " [r] retry" })] }));
77
+ }
78
+ return (_jsxs(Box, { flexDirection: "column", alignItems: "center", paddingY: 2, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "\u26A0" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "red", bold: true, children: info.title }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "gray", children: info.message }) }), info.hint && (_jsx(Box, { marginBottom: 1, paddingX: 1, borderStyle: "single", borderColor: "gray", children: _jsx(Text, { dimColor: true, children: info.hint }) })), onRetry && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "cyan", children: "[r] Retry" }) }))] }));
79
+ }
80
+ export default ErrorState;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,166 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, it, expect } from 'vitest';
3
+ import { render } from 'ink-testing-library';
4
+ import { ErrorState } from './ErrorState.js';
5
+ describe('ErrorState', () => {
6
+ describe('HTTP Errors', () => {
7
+ it('renders 404 not found with setup hint', () => {
8
+ const error = {
9
+ type: 'http',
10
+ code: 404,
11
+ message: 'HTTP 404: Not Found',
12
+ };
13
+ const { lastFrame } = render(_jsx(ErrorState, { error: error }));
14
+ const output = lastFrame() || '';
15
+ expect(output).toContain('Not Set Up');
16
+ expect(output).toContain('needs configuration');
17
+ expect(output).toContain('tlc setup');
18
+ });
19
+ it('renders 500 server error', () => {
20
+ const error = {
21
+ type: 'http',
22
+ code: 500,
23
+ message: 'HTTP 500: Internal Server Error',
24
+ };
25
+ const { lastFrame } = render(_jsx(ErrorState, { error: error }));
26
+ const output = lastFrame() || '';
27
+ expect(output).toContain('Server Error');
28
+ expect(output).toContain('wrong on the server');
29
+ });
30
+ it('renders 401 unauthorized', () => {
31
+ const error = {
32
+ type: 'http',
33
+ code: 401,
34
+ message: 'HTTP 401: Unauthorized',
35
+ };
36
+ const { lastFrame } = render(_jsx(ErrorState, { error: error }));
37
+ const output = lastFrame() || '';
38
+ expect(output).toContain('Not Authorized');
39
+ expect(output).toContain('Authentication');
40
+ });
41
+ it('renders 403 forbidden', () => {
42
+ const error = {
43
+ type: 'http',
44
+ code: 403,
45
+ message: 'HTTP 403: Forbidden',
46
+ };
47
+ const { lastFrame } = render(_jsx(ErrorState, { error: error }));
48
+ const output = lastFrame() || '';
49
+ expect(output).toContain('Access Denied');
50
+ });
51
+ it('renders unknown HTTP error with code', () => {
52
+ const error = {
53
+ type: 'http',
54
+ code: 418,
55
+ message: "HTTP 418: I'm a teapot",
56
+ };
57
+ const { lastFrame } = render(_jsx(ErrorState, { error: error }));
58
+ const output = lastFrame() || '';
59
+ expect(output).toContain('HTTP Error 418');
60
+ });
61
+ });
62
+ describe('Network Errors', () => {
63
+ it('renders network error with server hint', () => {
64
+ const error = {
65
+ type: 'network',
66
+ message: 'Cannot reach the server',
67
+ };
68
+ const { lastFrame } = render(_jsx(ErrorState, { error: error }));
69
+ const output = lastFrame() || '';
70
+ expect(output).toContain('Connection Lost');
71
+ expect(output).toContain('Cannot reach');
72
+ expect(output).toContain('tlc server');
73
+ });
74
+ });
75
+ describe('Timeout Errors', () => {
76
+ it('renders timeout error', () => {
77
+ const error = {
78
+ type: 'timeout',
79
+ message: 'Request timed out',
80
+ };
81
+ const { lastFrame } = render(_jsx(ErrorState, { error: error }));
82
+ const output = lastFrame() || '';
83
+ expect(output).toContain('Request Timeout');
84
+ expect(output).toContain('too long');
85
+ });
86
+ });
87
+ describe('Parse Errors', () => {
88
+ it('renders parse error', () => {
89
+ const error = {
90
+ type: 'parse',
91
+ message: 'Invalid response from server',
92
+ };
93
+ const { lastFrame } = render(_jsx(ErrorState, { error: error }));
94
+ const output = lastFrame() || '';
95
+ expect(output).toContain('Invalid Response');
96
+ expect(output).toContain('unexpected data');
97
+ });
98
+ });
99
+ describe('Unknown Errors', () => {
100
+ it('renders unknown error', () => {
101
+ const error = {
102
+ type: 'unknown',
103
+ message: 'Something went wrong',
104
+ };
105
+ const { lastFrame } = render(_jsx(ErrorState, { error: error }));
106
+ const output = lastFrame() || '';
107
+ expect(output).toContain('Unknown Error');
108
+ expect(output).toContain('unexpected error');
109
+ });
110
+ });
111
+ describe('Retry Button', () => {
112
+ it('shows retry button when onRetry provided', () => {
113
+ const error = {
114
+ type: 'network',
115
+ message: 'Network error',
116
+ };
117
+ const { lastFrame } = render(_jsx(ErrorState, { error: error, onRetry: () => { } }));
118
+ const output = lastFrame() || '';
119
+ expect(output).toContain('Retry');
120
+ });
121
+ it('hides retry button when no onRetry', () => {
122
+ const error = {
123
+ type: 'network',
124
+ message: 'Network error',
125
+ };
126
+ const { lastFrame } = render(_jsx(ErrorState, { error: error }));
127
+ const output = lastFrame() || '';
128
+ expect(output).not.toContain('[r] Retry');
129
+ });
130
+ });
131
+ describe('Compact Mode', () => {
132
+ it('renders compact error with title only', () => {
133
+ const error = {
134
+ type: 'http',
135
+ code: 500,
136
+ message: 'HTTP 500: Internal Server Error',
137
+ };
138
+ const { lastFrame } = render(_jsx(ErrorState, { error: error, compact: true }));
139
+ const output = lastFrame() || '';
140
+ expect(output).toContain('Server Error');
141
+ // Should be shorter than full version
142
+ expect(output.split('\n').length).toBeLessThan(5);
143
+ });
144
+ it('shows compact retry hint', () => {
145
+ const error = {
146
+ type: 'network',
147
+ message: 'Network error',
148
+ };
149
+ const { lastFrame } = render(_jsx(ErrorState, { error: error, onRetry: () => { }, compact: true }));
150
+ const output = lastFrame() || '';
151
+ expect(output).toContain('[r] retry');
152
+ });
153
+ });
154
+ describe('Error Icon', () => {
155
+ it('shows warning icon', () => {
156
+ const error = {
157
+ type: 'unknown',
158
+ message: 'Error',
159
+ };
160
+ const { lastFrame } = render(_jsx(ErrorState, { error: error }));
161
+ const output = lastFrame() || '';
162
+ // Warning triangle unicode or similar
163
+ expect(output.length).toBeGreaterThan(0);
164
+ });
165
+ });
166
+ });
@@ -10,6 +10,7 @@
10
10
  "dev": "tsx src/index.tsx",
11
11
  "build": "tsc",
12
12
  "start": "node dist/index.js",
13
+ "server": "node server/index.js",
13
14
  "test": "vitest run",
14
15
  "test:watch": "vitest",
15
16
  "test:coverage": "vitest run --coverage"
@@ -17,6 +18,7 @@
17
18
  "dependencies": {
18
19
  "chalk": "^5.3.0",
19
20
  "dockerode": "^4.0.2",
21
+ "express": "^4.22.1",
20
22
  "ink": "^5.0.1",
21
23
  "ink-spinner": "^5.0.0",
22
24
  "ink-text-input": "^6.0.0",
@@ -32,6 +34,7 @@
32
34
  "ink-testing-library": "^4.0.0",
33
35
  "jsdom": "^27.4.0",
34
36
  "memfs": "^4.56.10",
37
+ "supertest": "^7.2.2",
35
38
  "tsx": "^4.19.2",
36
39
  "typescript": "^5.7.2",
37
40
  "vitest": "^4.0.18"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tlc-claude-code",
3
- "version": "1.4.4",
3
+ "version": "1.4.6",
4
4
  "description": "TLC - Test Led Coding for Claude Code",
5
5
  "bin": {
6
6
  "tlc": "./bin/tlc.js",
@@ -55,5 +55,8 @@
55
55
  "@playwright/test": "^1.58.1",
56
56
  "playwright": "^1.58.1",
57
57
  "text-to-image": "^8.0.1"
58
+ },
59
+ "dependencies": {
60
+ "cookie-parser": "^1.4.7"
58
61
  }
59
62
  }