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.
- 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 +4 -1
- package/server/dashboard/index.html +284 -13
- package/server/dashboard/login.html +262 -0
- package/server/index.js +304 -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 +3 -3
- 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/plan-parser.js +59 -16
- 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,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-healing error boundary utilities for fetch operations.
|
|
3
|
+
* Provides consistent error handling, timeout support, and typed responses.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Maps error conditions to user-friendly error info
|
|
7
|
+
*/
|
|
8
|
+
function categorizeError(error, url) {
|
|
9
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
10
|
+
return {
|
|
11
|
+
type: 'timeout',
|
|
12
|
+
message: 'Request timed out',
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
if (error instanceof TypeError) {
|
|
16
|
+
// Network errors like failed to fetch
|
|
17
|
+
return {
|
|
18
|
+
type: 'network',
|
|
19
|
+
message: 'Cannot reach the server',
|
|
20
|
+
original: error,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (error instanceof SyntaxError) {
|
|
24
|
+
return {
|
|
25
|
+
type: 'parse',
|
|
26
|
+
message: 'Invalid response from server',
|
|
27
|
+
original: error,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (error instanceof Error) {
|
|
31
|
+
return {
|
|
32
|
+
type: 'unknown',
|
|
33
|
+
message: error.message,
|
|
34
|
+
original: error,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
type: 'unknown',
|
|
39
|
+
message: String(error),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Safe fetch wrapper that never throws.
|
|
44
|
+
* Always returns a FetchResult with either data or error.
|
|
45
|
+
*
|
|
46
|
+
* @param url - The URL to fetch
|
|
47
|
+
* @param options - Fetch options plus optional timeout (default 10s)
|
|
48
|
+
* @returns Promise<FetchResult<T>> - Always resolves, never rejects
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* const { data, error } = await safeFetch<RouterData>('/api/router');
|
|
53
|
+
* if (error) {
|
|
54
|
+
* // Handle error - render error state
|
|
55
|
+
* return <ErrorState error={error} onRetry={() => refresh()} />;
|
|
56
|
+
* }
|
|
57
|
+
* // Use data safely
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export async function safeFetch(url, options = {}) {
|
|
61
|
+
const { timeout = 10000, ...fetchOptions } = options;
|
|
62
|
+
try {
|
|
63
|
+
// Create abort controller for timeout
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
66
|
+
const response = await fetch(url, {
|
|
67
|
+
...fetchOptions,
|
|
68
|
+
signal: controller.signal,
|
|
69
|
+
});
|
|
70
|
+
clearTimeout(timeoutId);
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
return {
|
|
73
|
+
data: null,
|
|
74
|
+
error: {
|
|
75
|
+
type: 'http',
|
|
76
|
+
code: response.status,
|
|
77
|
+
message: `HTTP ${response.status}: ${response.statusText}`,
|
|
78
|
+
},
|
|
79
|
+
status: 'error',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const data = await response.json();
|
|
83
|
+
return {
|
|
84
|
+
data: data,
|
|
85
|
+
error: null,
|
|
86
|
+
status: 'success',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
console.error(`Fetch error for ${url}:`, err);
|
|
91
|
+
return {
|
|
92
|
+
data: null,
|
|
93
|
+
error: categorizeError(err, url),
|
|
94
|
+
status: 'error',
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* POST helper with JSON body
|
|
100
|
+
*/
|
|
101
|
+
export async function safePost(url, body, options = {}) {
|
|
102
|
+
return safeFetch(url, {
|
|
103
|
+
...options,
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: {
|
|
106
|
+
'Content-Type': 'application/json',
|
|
107
|
+
...options.headers,
|
|
108
|
+
},
|
|
109
|
+
body: JSON.stringify(body),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* PUT helper with JSON body
|
|
114
|
+
*/
|
|
115
|
+
export async function safePut(url, body, options = {}) {
|
|
116
|
+
return safeFetch(url, {
|
|
117
|
+
...options,
|
|
118
|
+
method: 'PUT',
|
|
119
|
+
headers: {
|
|
120
|
+
'Content-Type': 'application/json',
|
|
121
|
+
...options.headers,
|
|
122
|
+
},
|
|
123
|
+
body: JSON.stringify(body),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* DELETE helper
|
|
128
|
+
*/
|
|
129
|
+
export async function safeDelete(url, options = {}) {
|
|
130
|
+
return safeFetch(url, {
|
|
131
|
+
...options,
|
|
132
|
+
method: 'DELETE',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
export default safeFetch;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { safeFetch, safePost, safePut, safeDelete } from './safeFetch.js';
|
|
3
|
+
describe('safeFetch', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
vi.stubGlobal('fetch', vi.fn());
|
|
6
|
+
});
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
vi.unstubAllGlobals();
|
|
9
|
+
});
|
|
10
|
+
describe('successful requests', () => {
|
|
11
|
+
it('returns data on successful response', async () => {
|
|
12
|
+
const mockData = { message: 'success' };
|
|
13
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
14
|
+
ok: true,
|
|
15
|
+
json: () => Promise.resolve(mockData),
|
|
16
|
+
});
|
|
17
|
+
const result = await safeFetch('/api/test');
|
|
18
|
+
expect(result.status).toBe('success');
|
|
19
|
+
expect(result.data).toEqual(mockData);
|
|
20
|
+
expect(result.error).toBeNull();
|
|
21
|
+
});
|
|
22
|
+
it('passes through fetch options', async () => {
|
|
23
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
24
|
+
ok: true,
|
|
25
|
+
json: () => Promise.resolve({}),
|
|
26
|
+
});
|
|
27
|
+
await safeFetch('/api/test', {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: { 'X-Custom': 'value' },
|
|
30
|
+
});
|
|
31
|
+
expect(fetch).toHaveBeenCalledWith('/api/test', expect.objectContaining({
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: { 'X-Custom': 'value' },
|
|
34
|
+
}));
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('HTTP errors', () => {
|
|
38
|
+
it('handles 404 errors', async () => {
|
|
39
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
40
|
+
ok: false,
|
|
41
|
+
status: 404,
|
|
42
|
+
statusText: 'Not Found',
|
|
43
|
+
});
|
|
44
|
+
const result = await safeFetch('/api/missing');
|
|
45
|
+
expect(result.status).toBe('error');
|
|
46
|
+
expect(result.data).toBeNull();
|
|
47
|
+
expect(result.error).toEqual({
|
|
48
|
+
type: 'http',
|
|
49
|
+
code: 404,
|
|
50
|
+
message: 'HTTP 404: Not Found',
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
it('handles 500 errors', async () => {
|
|
54
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
55
|
+
ok: false,
|
|
56
|
+
status: 500,
|
|
57
|
+
statusText: 'Internal Server Error',
|
|
58
|
+
});
|
|
59
|
+
const result = await safeFetch('/api/broken');
|
|
60
|
+
expect(result.status).toBe('error');
|
|
61
|
+
expect(result.error?.type).toBe('http');
|
|
62
|
+
expect(result.error?.code).toBe(500);
|
|
63
|
+
});
|
|
64
|
+
it('handles 401 unauthorized', async () => {
|
|
65
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
66
|
+
ok: false,
|
|
67
|
+
status: 401,
|
|
68
|
+
statusText: 'Unauthorized',
|
|
69
|
+
});
|
|
70
|
+
const result = await safeFetch('/api/protected');
|
|
71
|
+
expect(result.error?.type).toBe('http');
|
|
72
|
+
expect(result.error?.code).toBe(401);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
describe('network errors', () => {
|
|
76
|
+
it('handles network failures', async () => {
|
|
77
|
+
vi.mocked(fetch).mockRejectedValue(new TypeError('Failed to fetch'));
|
|
78
|
+
const result = await safeFetch('/api/unreachable');
|
|
79
|
+
expect(result.status).toBe('error');
|
|
80
|
+
expect(result.error?.type).toBe('network');
|
|
81
|
+
expect(result.error?.message).toBe('Cannot reach the server');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
describe('timeout handling', () => {
|
|
85
|
+
it('times out after specified duration', async () => {
|
|
86
|
+
// Use AbortError to simulate timeout
|
|
87
|
+
vi.mocked(fetch).mockImplementation(() => {
|
|
88
|
+
const error = new DOMException('Aborted', 'AbortError');
|
|
89
|
+
return Promise.reject(error);
|
|
90
|
+
});
|
|
91
|
+
const result = await safeFetch('/api/slow', { timeout: 100 });
|
|
92
|
+
expect(result.status).toBe('error');
|
|
93
|
+
expect(result.error?.type).toBe('timeout');
|
|
94
|
+
expect(result.error?.message).toBe('Request timed out');
|
|
95
|
+
});
|
|
96
|
+
it('uses 10s default timeout', async () => {
|
|
97
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
98
|
+
ok: true,
|
|
99
|
+
json: () => Promise.resolve({}),
|
|
100
|
+
});
|
|
101
|
+
await safeFetch('/api/test');
|
|
102
|
+
// Signal should be passed
|
|
103
|
+
expect(fetch).toHaveBeenCalledWith('/api/test', expect.objectContaining({
|
|
104
|
+
signal: expect.any(AbortSignal),
|
|
105
|
+
}));
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe('parse errors', () => {
|
|
109
|
+
it('handles invalid JSON response', async () => {
|
|
110
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
111
|
+
ok: true,
|
|
112
|
+
json: () => Promise.reject(new SyntaxError('Unexpected token')),
|
|
113
|
+
});
|
|
114
|
+
const result = await safeFetch('/api/bad-json');
|
|
115
|
+
expect(result.status).toBe('error');
|
|
116
|
+
expect(result.error?.type).toBe('parse');
|
|
117
|
+
expect(result.error?.message).toBe('Invalid response from server');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
describe('never throws', () => {
|
|
121
|
+
it('always returns FetchResult, never throws', async () => {
|
|
122
|
+
// Various error types
|
|
123
|
+
const errorCases = [
|
|
124
|
+
new Error('Generic error'),
|
|
125
|
+
new TypeError('Type error'),
|
|
126
|
+
'String error',
|
|
127
|
+
undefined,
|
|
128
|
+
null,
|
|
129
|
+
];
|
|
130
|
+
for (const error of errorCases) {
|
|
131
|
+
vi.mocked(fetch).mockRejectedValue(error);
|
|
132
|
+
// Should not throw
|
|
133
|
+
const result = await safeFetch('/api/test');
|
|
134
|
+
expect(result).toBeDefined();
|
|
135
|
+
expect(result.status).toBe('error');
|
|
136
|
+
expect(result.data).toBeNull();
|
|
137
|
+
expect(result.error).not.toBeNull();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
describe('safePost', () => {
|
|
143
|
+
beforeEach(() => {
|
|
144
|
+
vi.stubGlobal('fetch', vi.fn());
|
|
145
|
+
});
|
|
146
|
+
afterEach(() => {
|
|
147
|
+
vi.unstubAllGlobals();
|
|
148
|
+
});
|
|
149
|
+
it('sends POST request with JSON body', async () => {
|
|
150
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
151
|
+
ok: true,
|
|
152
|
+
json: () => Promise.resolve({ id: 1 }),
|
|
153
|
+
});
|
|
154
|
+
const body = { name: 'test' };
|
|
155
|
+
await safePost('/api/items', body);
|
|
156
|
+
expect(fetch).toHaveBeenCalledWith('/api/items', expect.objectContaining({
|
|
157
|
+
method: 'POST',
|
|
158
|
+
headers: expect.objectContaining({
|
|
159
|
+
'Content-Type': 'application/json',
|
|
160
|
+
}),
|
|
161
|
+
body: JSON.stringify(body),
|
|
162
|
+
}));
|
|
163
|
+
});
|
|
164
|
+
it('merges custom headers', async () => {
|
|
165
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
166
|
+
ok: true,
|
|
167
|
+
json: () => Promise.resolve({}),
|
|
168
|
+
});
|
|
169
|
+
await safePost('/api/items', {}, { headers: { Authorization: 'Bearer token' } });
|
|
170
|
+
expect(fetch).toHaveBeenCalledWith('/api/items', expect.objectContaining({
|
|
171
|
+
headers: expect.objectContaining({
|
|
172
|
+
'Content-Type': 'application/json',
|
|
173
|
+
Authorization: 'Bearer token',
|
|
174
|
+
}),
|
|
175
|
+
}));
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
describe('safePut', () => {
|
|
179
|
+
beforeEach(() => {
|
|
180
|
+
vi.stubGlobal('fetch', vi.fn());
|
|
181
|
+
});
|
|
182
|
+
afterEach(() => {
|
|
183
|
+
vi.unstubAllGlobals();
|
|
184
|
+
});
|
|
185
|
+
it('sends PUT request with JSON body', async () => {
|
|
186
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
187
|
+
ok: true,
|
|
188
|
+
json: () => Promise.resolve({ updated: true }),
|
|
189
|
+
});
|
|
190
|
+
const body = { name: 'updated' };
|
|
191
|
+
await safePut('/api/items/1', body);
|
|
192
|
+
expect(fetch).toHaveBeenCalledWith('/api/items/1', expect.objectContaining({
|
|
193
|
+
method: 'PUT',
|
|
194
|
+
body: JSON.stringify(body),
|
|
195
|
+
}));
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
describe('safeDelete', () => {
|
|
199
|
+
beforeEach(() => {
|
|
200
|
+
vi.stubGlobal('fetch', vi.fn());
|
|
201
|
+
});
|
|
202
|
+
afterEach(() => {
|
|
203
|
+
vi.unstubAllGlobals();
|
|
204
|
+
});
|
|
205
|
+
it('sends DELETE request', async () => {
|
|
206
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
207
|
+
ok: true,
|
|
208
|
+
json: () => Promise.resolve({ deleted: true }),
|
|
209
|
+
});
|
|
210
|
+
await safeDelete('/api/items/1');
|
|
211
|
+
expect(fetch).toHaveBeenCalledWith('/api/items/1', expect.objectContaining({
|
|
212
|
+
method: 'DELETE',
|
|
213
|
+
}));
|
|
214
|
+
});
|
|
215
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tasks API - Returns tasks from PLAN.md files in flat array format
|
|
3
|
+
*/
|
|
4
|
+
import type { Dirent } from 'fs';
|
|
5
|
+
export interface Task {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
status: 'pending' | 'in_progress' | 'completed';
|
|
9
|
+
owner: string | null;
|
|
10
|
+
phase: number;
|
|
11
|
+
}
|
|
12
|
+
export interface FileSystem {
|
|
13
|
+
existsSync: (path: string) => boolean;
|
|
14
|
+
readdir: (path: string, options: {
|
|
15
|
+
withFileTypes: true;
|
|
16
|
+
}) => Promise<Dirent[]>;
|
|
17
|
+
readFile: (path: string, encoding: BufferEncoding) => Promise<string>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Get all tasks from all PLAN.md files in the planning directory
|
|
21
|
+
* @param projectPath - Root path of the project
|
|
22
|
+
* @param fs - File system implementation (for testing)
|
|
23
|
+
* @returns Array of tasks in flat format, sorted by phase then task number
|
|
24
|
+
*/
|
|
25
|
+
export declare function getTasks(projectPath: string, fs?: FileSystem): Promise<Task[]>;
|
|
26
|
+
/**
|
|
27
|
+
* Parse tasks from PLAN.md content
|
|
28
|
+
* @param content - Content of the PLAN.md file
|
|
29
|
+
* @param phaseNum - Phase number for task ID generation
|
|
30
|
+
* @returns Array of tasks parsed from the content
|
|
31
|
+
*/
|
|
32
|
+
export declare function parseTasksFromPlan(content: string, phaseNum: number): Task[];
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tasks API - Returns tasks from PLAN.md files in flat array format
|
|
3
|
+
*/
|
|
4
|
+
import { readdir, readFile } from 'fs/promises';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { existsSync } from 'fs';
|
|
7
|
+
// Default file system implementation
|
|
8
|
+
const defaultFs = {
|
|
9
|
+
existsSync,
|
|
10
|
+
readdir: (path, options) => readdir(path, options),
|
|
11
|
+
readFile: (path, encoding) => readFile(path, encoding),
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Get all tasks from all PLAN.md files in the planning directory
|
|
15
|
+
* @param projectPath - Root path of the project
|
|
16
|
+
* @param fs - File system implementation (for testing)
|
|
17
|
+
* @returns Array of tasks in flat format, sorted by phase then task number
|
|
18
|
+
*/
|
|
19
|
+
export async function getTasks(projectPath, fs = defaultFs) {
|
|
20
|
+
const phasesDir = join(projectPath, '.planning', 'phases');
|
|
21
|
+
if (!fs.existsSync(phasesDir)) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
const tasks = [];
|
|
25
|
+
try {
|
|
26
|
+
const entries = await fs.readdir(phasesDir, { withFileTypes: true });
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (entry.isDirectory()) {
|
|
29
|
+
// Extract phase number from directory name (e.g., "39-fix-api" -> 39)
|
|
30
|
+
const dirMatch = entry.name.match(/^(\d+)/);
|
|
31
|
+
if (dirMatch) {
|
|
32
|
+
const phaseNum = parseInt(dirMatch[1], 10);
|
|
33
|
+
const planPath = join(phasesDir, entry.name, `${dirMatch[1]}-PLAN.md`);
|
|
34
|
+
if (fs.existsSync(planPath)) {
|
|
35
|
+
try {
|
|
36
|
+
const content = await fs.readFile(planPath, 'utf-8');
|
|
37
|
+
const phaseTasks = parseTasksFromPlan(content, phaseNum);
|
|
38
|
+
tasks.push(...phaseTasks);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Skip unreadable files
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Return empty if can't read directory
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
// Sort by phase number, then by task number
|
|
53
|
+
tasks.sort((a, b) => {
|
|
54
|
+
if (a.phase !== b.phase) {
|
|
55
|
+
return a.phase - b.phase;
|
|
56
|
+
}
|
|
57
|
+
// Extract task number from id (e.g., "39-1" -> 1)
|
|
58
|
+
const aTaskNum = parseInt(a.id.split('-')[1], 10);
|
|
59
|
+
const bTaskNum = parseInt(b.id.split('-')[1], 10);
|
|
60
|
+
return aTaskNum - bTaskNum;
|
|
61
|
+
});
|
|
62
|
+
return tasks;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Parse tasks from PLAN.md content
|
|
66
|
+
* @param content - Content of the PLAN.md file
|
|
67
|
+
* @param phaseNum - Phase number for task ID generation
|
|
68
|
+
* @returns Array of tasks parsed from the content
|
|
69
|
+
*/
|
|
70
|
+
export function parseTasksFromPlan(content, phaseNum) {
|
|
71
|
+
const tasks = [];
|
|
72
|
+
const lines = content.split('\n');
|
|
73
|
+
for (const line of lines) {
|
|
74
|
+
// Match task headers like:
|
|
75
|
+
// "### Task 1: Setup Project [x@alice]"
|
|
76
|
+
// "### Task 1: Setup Project [>]"
|
|
77
|
+
// "### Task 1: Setup Project [ ]"
|
|
78
|
+
// "### Task 1: Name [x]"
|
|
79
|
+
// Requires colon after task number
|
|
80
|
+
const taskMatch = line.match(/^###\s*Task\s+(\d+):\s*(.+?)\s*\[([x> ]?)(?:@(\w+))?\]\s*$/i);
|
|
81
|
+
if (taskMatch) {
|
|
82
|
+
const taskNum = parseInt(taskMatch[1], 10);
|
|
83
|
+
const title = taskMatch[2].trim();
|
|
84
|
+
const statusChar = taskMatch[3].trim();
|
|
85
|
+
const owner = taskMatch[4] || null;
|
|
86
|
+
const status = statusChar === 'x' ? 'completed' :
|
|
87
|
+
statusChar === '>' ? 'in_progress' : 'pending';
|
|
88
|
+
tasks.push({
|
|
89
|
+
id: `${phaseNum}-${taskNum}`,
|
|
90
|
+
title,
|
|
91
|
+
status,
|
|
92
|
+
owner,
|
|
93
|
+
phase: phaseNum,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return tasks;
|
|
98
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|