tlc-claude-code 0.6.3 → 0.7.0
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/CLAUDE.md +59 -0
- package/README.md +240 -64
- package/autofix.md +327 -0
- package/bin/install.js +24 -3
- package/bug.md +255 -0
- package/build.md +167 -21
- package/ci.md +414 -0
- package/claim.md +189 -0
- package/config.md +236 -0
- package/deploy.md +516 -0
- package/docs.md +494 -0
- package/edge-cases.md +340 -0
- package/export.md +456 -0
- package/help.md +84 -1
- package/init.md +56 -7
- package/issues.md +376 -0
- package/new-project.md +68 -4
- package/package.json +4 -2
- package/plan.md +15 -1
- package/progress.md +17 -0
- package/quality.md +273 -0
- package/release.md +135 -0
- package/server/dashboard/index.html +708 -0
- package/server/index.js +406 -0
- package/server/lib/plan-parser.js +146 -0
- package/server/lib/project-detector.js +301 -0
- package/server/package.json +19 -0
- package/server.md +742 -0
- package/who.md +151 -0
package/server/index.js
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const { createServer } = require('http');
|
|
5
|
+
const { WebSocketServer } = require('ws');
|
|
6
|
+
const { createProxyMiddleware } = require('http-proxy-middleware');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const { spawn } = require('child_process');
|
|
10
|
+
const chokidar = require('chokidar');
|
|
11
|
+
|
|
12
|
+
const { detectProject } = require('./lib/project-detector');
|
|
13
|
+
const { parsePlan, parseBugs } = require('./lib/plan-parser');
|
|
14
|
+
|
|
15
|
+
// Configuration
|
|
16
|
+
const TLC_PORT = parseInt(process.env.TLC_PORT || '3147');
|
|
17
|
+
const PROJECT_DIR = process.cwd();
|
|
18
|
+
|
|
19
|
+
// State
|
|
20
|
+
let appProcess = null;
|
|
21
|
+
let appPort = 3000;
|
|
22
|
+
let wsClients = new Set();
|
|
23
|
+
const logs = { app: [], test: [], git: [] };
|
|
24
|
+
|
|
25
|
+
// Create Express app
|
|
26
|
+
const app = express();
|
|
27
|
+
const server = createServer(app);
|
|
28
|
+
const wss = new WebSocketServer({ server });
|
|
29
|
+
|
|
30
|
+
// Middleware
|
|
31
|
+
app.use(express.json());
|
|
32
|
+
app.use(express.static(path.join(__dirname, 'dashboard')));
|
|
33
|
+
|
|
34
|
+
// Broadcast to all WebSocket clients
|
|
35
|
+
function broadcast(type, data) {
|
|
36
|
+
const message = JSON.stringify({ type, data });
|
|
37
|
+
wsClients.forEach(client => {
|
|
38
|
+
if (client.readyState === 1) { // OPEN
|
|
39
|
+
client.send(message);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Add log entry
|
|
45
|
+
function addLog(type, text, level = '') {
|
|
46
|
+
const entry = { text, level, time: new Date().toISOString() };
|
|
47
|
+
logs[type].push(entry);
|
|
48
|
+
if (logs[type].length > 1000) logs[type].shift();
|
|
49
|
+
broadcast(`${type}-log`, { data: text, level });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// WebSocket connection handling
|
|
53
|
+
wss.on('connection', (ws) => {
|
|
54
|
+
wsClients.add(ws);
|
|
55
|
+
console.log(`[TLC] Client connected (${wsClients.size} total)`);
|
|
56
|
+
|
|
57
|
+
// Send recent logs to new client
|
|
58
|
+
ws.send(JSON.stringify({ type: 'init', data: { logs, appPort } }));
|
|
59
|
+
|
|
60
|
+
ws.on('close', () => {
|
|
61
|
+
wsClients.delete(ws);
|
|
62
|
+
console.log(`[TLC] Client disconnected (${wsClients.size} total)`);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Start the user's app
|
|
67
|
+
async function startApp() {
|
|
68
|
+
const project = detectProject(PROJECT_DIR);
|
|
69
|
+
|
|
70
|
+
if (!project) {
|
|
71
|
+
addLog('app', 'Could not detect project type. Create a start command in .tlc.json', 'error');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
appPort = project.port;
|
|
76
|
+
addLog('app', `Detected: ${project.name}`, 'info');
|
|
77
|
+
addLog('app', `Command: ${project.cmd} ${project.args.join(' ')}`, 'info');
|
|
78
|
+
addLog('app', `Port: ${appPort}`, 'info');
|
|
79
|
+
|
|
80
|
+
// Kill existing process if any
|
|
81
|
+
if (appProcess) {
|
|
82
|
+
appProcess.kill();
|
|
83
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
appProcess = spawn(project.cmd, project.args, {
|
|
87
|
+
cwd: PROJECT_DIR,
|
|
88
|
+
env: { ...process.env, PORT: appPort.toString() },
|
|
89
|
+
shell: true
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
appProcess.stdout.on('data', (data) => {
|
|
93
|
+
const text = data.toString().trim();
|
|
94
|
+
if (text) addLog('app', text);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
appProcess.stderr.on('data', (data) => {
|
|
98
|
+
const text = data.toString().trim();
|
|
99
|
+
if (text) addLog('app', text, 'error');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
appProcess.on('exit', (code) => {
|
|
103
|
+
addLog('app', `App exited with code ${code}`, code === 0 ? 'info' : 'error');
|
|
104
|
+
appProcess = null;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
broadcast('app-start', { port: appPort });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Run tests
|
|
111
|
+
function runTests() {
|
|
112
|
+
addLog('test', '--- Running tests ---', 'info');
|
|
113
|
+
|
|
114
|
+
// Try to detect test command
|
|
115
|
+
let testCmd = 'npm';
|
|
116
|
+
let testArgs = ['test'];
|
|
117
|
+
|
|
118
|
+
const pkgPath = path.join(PROJECT_DIR, 'package.json');
|
|
119
|
+
if (fs.existsSync(pkgPath)) {
|
|
120
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
121
|
+
if (pkg.scripts?.test) {
|
|
122
|
+
testCmd = 'npm';
|
|
123
|
+
testArgs = ['test'];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const testProcess = spawn(testCmd, testArgs, {
|
|
128
|
+
cwd: PROJECT_DIR,
|
|
129
|
+
env: { ...process.env, CI: 'true' },
|
|
130
|
+
shell: true
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
testProcess.stdout.on('data', (data) => {
|
|
134
|
+
const text = data.toString().trim();
|
|
135
|
+
if (text) {
|
|
136
|
+
broadcast('test-output', { data: text, stream: 'stdout' });
|
|
137
|
+
addLog('test', text);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
testProcess.stderr.on('data', (data) => {
|
|
142
|
+
const text = data.toString().trim();
|
|
143
|
+
if (text) {
|
|
144
|
+
broadcast('test-output', { data: text, stream: 'stderr' });
|
|
145
|
+
addLog('test', text, 'error');
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
testProcess.on('exit', (code) => {
|
|
150
|
+
broadcast('test-complete', { exitCode: code });
|
|
151
|
+
addLog('test', `Tests ${code === 0 ? 'passed' : 'failed'}`, code === 0 ? 'success' : 'error');
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// API Routes
|
|
156
|
+
app.get('/api/status', (req, res) => {
|
|
157
|
+
const bugs = parseBugs(PROJECT_DIR);
|
|
158
|
+
const plan = parsePlan(PROJECT_DIR);
|
|
159
|
+
|
|
160
|
+
res.json({
|
|
161
|
+
appRunning: appProcess !== null,
|
|
162
|
+
appPort,
|
|
163
|
+
testsPass: plan.testsPass || 0,
|
|
164
|
+
testsFail: plan.testsFail || 0,
|
|
165
|
+
bugsOpen: bugs.filter(b => b.status === 'open').length,
|
|
166
|
+
phase: plan.currentPhase,
|
|
167
|
+
phaseName: plan.currentPhaseName
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
app.get('/api/logs/:type', (req, res) => {
|
|
172
|
+
const type = req.params.type;
|
|
173
|
+
if (logs[type]) {
|
|
174
|
+
res.json(logs[type]);
|
|
175
|
+
} else {
|
|
176
|
+
res.status(404).json({ error: 'Unknown log type' });
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
app.get('/api/tasks', (req, res) => {
|
|
181
|
+
const plan = parsePlan(PROJECT_DIR);
|
|
182
|
+
res.json({
|
|
183
|
+
phase: plan.currentPhase,
|
|
184
|
+
phaseName: plan.currentPhaseName,
|
|
185
|
+
items: plan.tasks
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
app.post('/api/bug', (req, res) => {
|
|
190
|
+
const { description, url, screenshot, severity } = req.body;
|
|
191
|
+
|
|
192
|
+
if (!description) {
|
|
193
|
+
return res.status(400).json({ error: 'Description required' });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const bugsFile = path.join(PROJECT_DIR, '.planning', 'BUGS.md');
|
|
197
|
+
|
|
198
|
+
// Generate bug ID
|
|
199
|
+
const bugs = parseBugs(PROJECT_DIR);
|
|
200
|
+
const nextId = bugs.length + 1;
|
|
201
|
+
const bugId = `BUG-${String(nextId).padStart(3, '0')}`;
|
|
202
|
+
|
|
203
|
+
// Create bug entry
|
|
204
|
+
const timestamp = new Date().toISOString().split('T')[0];
|
|
205
|
+
const bugEntry = `
|
|
206
|
+
### ${bugId}: ${description.split('\n')[0].slice(0, 50)} [open]
|
|
207
|
+
|
|
208
|
+
- **Reported:** ${timestamp}
|
|
209
|
+
- **Severity:** ${severity || 'medium'}
|
|
210
|
+
- **URL:** ${url || 'N/A'}
|
|
211
|
+
${screenshot ? `- **Screenshot:** screenshots/${bugId}.png` : ''}
|
|
212
|
+
|
|
213
|
+
${description}
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
`;
|
|
217
|
+
|
|
218
|
+
// Ensure .planning directory exists
|
|
219
|
+
const planningDir = path.join(PROJECT_DIR, '.planning');
|
|
220
|
+
if (!fs.existsSync(planningDir)) {
|
|
221
|
+
fs.mkdirSync(planningDir, { recursive: true });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Save screenshot if provided
|
|
225
|
+
if (screenshot && screenshot.startsWith('data:image')) {
|
|
226
|
+
const screenshotDir = path.join(planningDir, 'screenshots');
|
|
227
|
+
if (!fs.existsSync(screenshotDir)) {
|
|
228
|
+
fs.mkdirSync(screenshotDir, { recursive: true });
|
|
229
|
+
}
|
|
230
|
+
const base64Data = screenshot.split(',')[1];
|
|
231
|
+
fs.writeFileSync(
|
|
232
|
+
path.join(screenshotDir, `${bugId}.png`),
|
|
233
|
+
Buffer.from(base64Data, 'base64')
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Append to BUGS.md
|
|
238
|
+
let content = '';
|
|
239
|
+
if (fs.existsSync(bugsFile)) {
|
|
240
|
+
content = fs.readFileSync(bugsFile, 'utf-8');
|
|
241
|
+
} else {
|
|
242
|
+
content = `# Bug Tracker
|
|
243
|
+
|
|
244
|
+
## Open Bugs
|
|
245
|
+
|
|
246
|
+
`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Insert after "## Open Bugs" heading
|
|
250
|
+
const insertPoint = content.indexOf('## Open Bugs');
|
|
251
|
+
if (insertPoint !== -1) {
|
|
252
|
+
const afterHeading = content.indexOf('\n', insertPoint) + 1;
|
|
253
|
+
content = content.slice(0, afterHeading) + bugEntry + content.slice(afterHeading);
|
|
254
|
+
} else {
|
|
255
|
+
content += bugEntry;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
fs.writeFileSync(bugsFile, content);
|
|
259
|
+
|
|
260
|
+
broadcast('bug-created', { bugId, description: description.slice(0, 50) });
|
|
261
|
+
addLog('app', `Bug ${bugId} created`, 'warn');
|
|
262
|
+
|
|
263
|
+
res.json({ success: true, bugId });
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
app.post('/api/test', (req, res) => {
|
|
267
|
+
runTests();
|
|
268
|
+
res.json({ success: true });
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
app.post('/api/restart', (req, res) => {
|
|
272
|
+
addLog('app', '--- Restarting app ---', 'warn');
|
|
273
|
+
broadcast('app-restart', {});
|
|
274
|
+
startApp();
|
|
275
|
+
res.json({ success: true });
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Proxy to running app
|
|
279
|
+
app.use('/app', createProxyMiddleware({
|
|
280
|
+
target: () => `http://localhost:${appPort}`,
|
|
281
|
+
changeOrigin: true,
|
|
282
|
+
pathRewrite: { '^/app': '' },
|
|
283
|
+
ws: true,
|
|
284
|
+
onError: (err, req, res) => {
|
|
285
|
+
res.status(502).send(`
|
|
286
|
+
<html>
|
|
287
|
+
<body style="font-family: system-ui; padding: 40px; background: #1a1a2e; color: #eee;">
|
|
288
|
+
<h2>App not running</h2>
|
|
289
|
+
<p>Waiting for app to start on port ${appPort}...</p>
|
|
290
|
+
<script>setTimeout(() => location.reload(), 2000)</script>
|
|
291
|
+
</body>
|
|
292
|
+
</html>
|
|
293
|
+
`);
|
|
294
|
+
}
|
|
295
|
+
}));
|
|
296
|
+
|
|
297
|
+
// File watching
|
|
298
|
+
function setupWatchers() {
|
|
299
|
+
// Watch source files
|
|
300
|
+
const sourceWatcher = chokidar.watch(
|
|
301
|
+
['src', 'lib', 'app', 'pages', 'components'].map(d => path.join(PROJECT_DIR, d)),
|
|
302
|
+
{ ignored: /node_modules/, ignoreInitial: true }
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
sourceWatcher.on('change', (filePath) => {
|
|
306
|
+
const relative = path.relative(PROJECT_DIR, filePath);
|
|
307
|
+
addLog('app', `File changed: ${relative}`, 'info');
|
|
308
|
+
broadcast('file-change', { path: relative });
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Watch git activity
|
|
312
|
+
const gitWatcher = chokidar.watch(path.join(PROJECT_DIR, '.git/logs/HEAD'), {
|
|
313
|
+
ignoreInitial: true
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
gitWatcher.on('change', () => {
|
|
317
|
+
// Parse last git log entry
|
|
318
|
+
try {
|
|
319
|
+
const logPath = path.join(PROJECT_DIR, '.git/logs/HEAD');
|
|
320
|
+
const content = fs.readFileSync(logPath, 'utf-8');
|
|
321
|
+
const lines = content.trim().split('\n');
|
|
322
|
+
const lastLine = lines[lines.length - 1];
|
|
323
|
+
const match = lastLine.match(/\t(.+)$/);
|
|
324
|
+
if (match) {
|
|
325
|
+
addLog('git', match[1], 'info');
|
|
326
|
+
broadcast('git-activity', { entry: match[1] });
|
|
327
|
+
}
|
|
328
|
+
} catch (e) {
|
|
329
|
+
// Ignore errors
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Watch planning files
|
|
334
|
+
const planWatcher = chokidar.watch(path.join(PROJECT_DIR, '.planning'), {
|
|
335
|
+
ignoreInitial: true
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
planWatcher.on('change', (filePath) => {
|
|
339
|
+
if (filePath.includes('PLAN.md')) {
|
|
340
|
+
broadcast('task-update', {});
|
|
341
|
+
}
|
|
342
|
+
if (filePath.includes('BUGS.md')) {
|
|
343
|
+
broadcast('bug-update', {});
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Graceful shutdown
|
|
349
|
+
function shutdown() {
|
|
350
|
+
console.log('\n[TLC] Shutting down...');
|
|
351
|
+
|
|
352
|
+
if (appProcess) {
|
|
353
|
+
appProcess.kill();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
wsClients.forEach(client => client.close());
|
|
357
|
+
server.close(() => {
|
|
358
|
+
console.log('[TLC] Server stopped');
|
|
359
|
+
process.exit(0);
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
process.on('SIGINT', shutdown);
|
|
364
|
+
process.on('SIGTERM', shutdown);
|
|
365
|
+
|
|
366
|
+
// Start server
|
|
367
|
+
async function main() {
|
|
368
|
+
console.log(`
|
|
369
|
+
████████╗██╗ ██████╗
|
|
370
|
+
╚══██╔══╝██║ ██╔════╝
|
|
371
|
+
██║ ██║ ██║
|
|
372
|
+
██║ ██║ ██║
|
|
373
|
+
██║ ███████╗╚██████╗
|
|
374
|
+
╚═╝ ╚══════╝ ╚═════╝
|
|
375
|
+
|
|
376
|
+
TLC Dev Server
|
|
377
|
+
`);
|
|
378
|
+
|
|
379
|
+
server.listen(TLC_PORT, () => {
|
|
380
|
+
console.log(` Dashboard: http://localhost:${TLC_PORT}`);
|
|
381
|
+
console.log(` Share: http://${getLocalIP()}:${TLC_PORT}`);
|
|
382
|
+
console.log('');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
setupWatchers();
|
|
386
|
+
await startApp();
|
|
387
|
+
|
|
388
|
+
console.log(' Press Ctrl+C to stop\n');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Get local IP for sharing
|
|
392
|
+
function getLocalIP() {
|
|
393
|
+
const { networkInterfaces } = require('os');
|
|
394
|
+
const nets = networkInterfaces();
|
|
395
|
+
|
|
396
|
+
for (const name of Object.keys(nets)) {
|
|
397
|
+
for (const net of nets[name]) {
|
|
398
|
+
if (net.family === 'IPv4' && !net.internal) {
|
|
399
|
+
return net.address;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return 'localhost';
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse tasks from PLAN.md files
|
|
6
|
+
*/
|
|
7
|
+
function parsePlan(projectDir) {
|
|
8
|
+
const result = {
|
|
9
|
+
currentPhase: null,
|
|
10
|
+
currentPhaseName: '',
|
|
11
|
+
tasks: [],
|
|
12
|
+
testsPass: 0,
|
|
13
|
+
testsFail: 0
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Find current phase from ROADMAP.md
|
|
17
|
+
const roadmapPath = path.join(projectDir, '.planning', 'ROADMAP.md');
|
|
18
|
+
if (fs.existsSync(roadmapPath)) {
|
|
19
|
+
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
|
20
|
+
|
|
21
|
+
// Find first incomplete phase
|
|
22
|
+
const phaseMatches = content.matchAll(/##\s+Phase\s+(\d+)(?:\.(\d+))?[:\s]+(.+?)(?:\s*\[([x ])\])?$/gm);
|
|
23
|
+
for (const match of phaseMatches) {
|
|
24
|
+
const phaseNum = match[2] ? `${match[1]}.${match[2]}` : match[1];
|
|
25
|
+
const phaseName = match[3].trim();
|
|
26
|
+
const completed = match[4] === 'x';
|
|
27
|
+
|
|
28
|
+
if (!completed) {
|
|
29
|
+
result.currentPhase = phaseNum;
|
|
30
|
+
result.currentPhaseName = phaseName;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Load current phase PLAN.md
|
|
37
|
+
if (result.currentPhase) {
|
|
38
|
+
const planPath = path.join(
|
|
39
|
+
projectDir,
|
|
40
|
+
'.planning',
|
|
41
|
+
'phases',
|
|
42
|
+
`${result.currentPhase}-PLAN.md`
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
if (fs.existsSync(planPath)) {
|
|
46
|
+
const content = fs.readFileSync(planPath, 'utf-8');
|
|
47
|
+
result.tasks = parseTasksFromPlan(content);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse task entries from PLAN.md content
|
|
56
|
+
* Supports formats:
|
|
57
|
+
* ### Task 1: Title [ ]
|
|
58
|
+
* ### Task 1: Title [>@user]
|
|
59
|
+
* ### Task 1: Title [x@user]
|
|
60
|
+
*/
|
|
61
|
+
function parseTasksFromPlan(content) {
|
|
62
|
+
const tasks = [];
|
|
63
|
+
const taskRegex = /###\s+Task\s+(\d+)[:\s]+(.+?)\s*\[([^\]]*)\]/g;
|
|
64
|
+
|
|
65
|
+
let match;
|
|
66
|
+
while ((match = taskRegex.exec(content)) !== null) {
|
|
67
|
+
const [, num, title, statusMarker] = match;
|
|
68
|
+
|
|
69
|
+
let status = 'available';
|
|
70
|
+
let owner = null;
|
|
71
|
+
|
|
72
|
+
if (statusMarker.startsWith('x')) {
|
|
73
|
+
status = 'done';
|
|
74
|
+
const ownerMatch = statusMarker.match(/@(\w+)/);
|
|
75
|
+
if (ownerMatch) owner = ownerMatch[1];
|
|
76
|
+
} else if (statusMarker.startsWith('>')) {
|
|
77
|
+
status = 'working';
|
|
78
|
+
const ownerMatch = statusMarker.match(/@(\w+)/);
|
|
79
|
+
if (ownerMatch) owner = ownerMatch[1];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
tasks.push({
|
|
83
|
+
num: parseInt(num),
|
|
84
|
+
title: title.trim(),
|
|
85
|
+
status,
|
|
86
|
+
owner
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return tasks;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Parse bugs from BUGS.md
|
|
95
|
+
*/
|
|
96
|
+
function parseBugs(projectDir) {
|
|
97
|
+
const bugs = [];
|
|
98
|
+
const bugsPath = path.join(projectDir, '.planning', 'BUGS.md');
|
|
99
|
+
|
|
100
|
+
if (!fs.existsSync(bugsPath)) {
|
|
101
|
+
return bugs;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const content = fs.readFileSync(bugsPath, 'utf-8');
|
|
105
|
+
|
|
106
|
+
// Match bug entries: ### BUG-001: Title [status]
|
|
107
|
+
const bugRegex = /###\s+(BUG-\d+)[:\s]+(.+?)\s*\[(\w+)\]/g;
|
|
108
|
+
|
|
109
|
+
let match;
|
|
110
|
+
while ((match = bugRegex.exec(content)) !== null) {
|
|
111
|
+
const [, id, title, status] = match;
|
|
112
|
+
bugs.push({
|
|
113
|
+
id,
|
|
114
|
+
title: title.trim(),
|
|
115
|
+
status: status.toLowerCase()
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return bugs;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get username for task claiming
|
|
124
|
+
*/
|
|
125
|
+
function getUsername() {
|
|
126
|
+
// Check TLC_USER env var first
|
|
127
|
+
if (process.env.TLC_USER) {
|
|
128
|
+
return process.env.TLC_USER.toLowerCase().replace(/\s+/g, '-');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Try git config
|
|
132
|
+
try {
|
|
133
|
+
const { execSync } = require('child_process');
|
|
134
|
+
const gitUser = execSync('git config user.name', { encoding: 'utf-8' }).trim();
|
|
135
|
+
if (gitUser) {
|
|
136
|
+
return gitUser.toLowerCase().split(' ')[0]; // First name only
|
|
137
|
+
}
|
|
138
|
+
} catch (e) {
|
|
139
|
+
// Ignore
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Fall back to system user
|
|
143
|
+
return require('os').userInfo().username || 'unknown';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = { parsePlan, parseBugs, getUsername };
|