tlc-claude-code 1.8.3 → 1.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tlc-claude-code",
3
- "version": "1.8.3",
3
+ "version": "1.8.5",
4
4
  "description": "TLC - Test Led Coding for Claude Code",
5
5
  "bin": {
6
6
  "tlc": "./bin/tlc.js",
package/server/index.js CHANGED
@@ -57,6 +57,7 @@ const EXTERNAL_APP_PORT = parseInt(process.env.TLC_APP_PORT || '5000');
57
57
  // State
58
58
  let appProcess = null;
59
59
  let appPort = 3000;
60
+ let appIsDocker = false; // true when app is Docker-managed (no local process)
60
61
  let wsClients = new Set();
61
62
  const logs = { app: [], test: [], git: [] };
62
63
  const commandHistory = [];
@@ -485,6 +486,19 @@ async function startApp() {
485
486
 
486
487
  appPort = project.port;
487
488
  addLog('app', `Detected: ${project.name}`, 'info');
489
+
490
+ // Docker-managed apps: don't spawn, just proxy
491
+ if (project.type === 'docker') {
492
+ appIsDocker = true;
493
+ addLog('app', `App is Docker-managed — proxying to port ${appPort}`, 'info');
494
+ if (project.url) {
495
+ addLog('app', `App URL: ${project.url}`, 'info');
496
+ }
497
+ addLog('app', 'TLC will not spawn the app. Use Docker to manage it.', 'info');
498
+ broadcast('app-start', { port: appPort });
499
+ return;
500
+ }
501
+
488
502
  addLog('app', `Command: ${project.cmd} ${project.args.join(' ')}`, 'info');
489
503
  addLog('app', `Port: ${appPort}`, 'info');
490
504
 
@@ -635,10 +649,22 @@ app.get('/api/project', (req, res) => {
635
649
  const roadmapPath = path.join(PROJECT_DIR, '.planning', 'ROADMAP.md');
636
650
  if (fs.existsSync(roadmapPath)) {
637
651
  const content = fs.readFileSync(roadmapPath, 'utf-8');
638
- const phases = content.match(/##\s+Phase\s+\d+/g) || [];
639
- totalPhases = phases.length;
640
- const completed = content.match(/##\s+Phase\s+\d+[^[]*\[x\]/gi) || [];
641
- completedPhases = completed.length;
652
+
653
+ // Format 1: ## Phase N heading format
654
+ const headingPhases = content.match(/##\s+Phase\s+\d+/g) || [];
655
+ const headingCompleted = content.match(/##\s+Phase\s+\d+[^[]*\[x\]/gi) || [];
656
+
657
+ // Format 2: Table format | N | [Name](link) | status |
658
+ const tablePhases = content.match(/\|\s*\d+\s*\|\s*\[[^\]]+\][^\|]*\|\s*\w+\s*\|/g) || [];
659
+ const tableCompleted = (content.match(/\|\s*\d+\s*\|\s*\[[^\]]+\][^\|]*\|\s*(?:complete|done|verified)\s*\|/gi) || []);
660
+
661
+ if (headingPhases.length > 0) {
662
+ totalPhases = headingPhases.length;
663
+ completedPhases = headingCompleted.length;
664
+ } else if (tablePhases.length > 0) {
665
+ totalPhases = tablePhases.length;
666
+ completedPhases = tableCompleted.length;
667
+ }
642
668
  }
643
669
 
644
670
  // Calculate progress
@@ -673,7 +699,7 @@ app.get('/api/status', (req, res) => {
673
699
  const plan = parsePlan(PROJECT_DIR);
674
700
 
675
701
  res.json({
676
- appRunning: appProcess !== null,
702
+ appRunning: appProcess !== null || appIsDocker,
677
703
  appPort,
678
704
  testsPass: plan.testsPass || 0,
679
705
  testsFail: plan.testsFail || 0,
@@ -1087,7 +1113,7 @@ app.get('/api/health', (req, res) => {
1087
1113
  heapUsed: memUsage.heapUsed,
1088
1114
  heapTotal: memUsage.heapTotal,
1089
1115
  },
1090
- appRunning: appProcess !== null,
1116
+ appRunning: appProcess !== null || appIsDocker,
1091
1117
  appPort,
1092
1118
  });
1093
1119
  });
@@ -1486,7 +1512,7 @@ function getHealthData() {
1486
1512
  memory: memUsed,
1487
1513
  cpu: Math.min(cpuPercent, 100),
1488
1514
  uptime: process.uptime(),
1489
- appRunning: appProcess !== null,
1515
+ appRunning: appProcess !== null || appIsDocker,
1490
1516
  appPort: appPort
1491
1517
  };
1492
1518
  }
@@ -18,7 +18,7 @@ function parsePlan(projectDir) {
18
18
  if (fs.existsSync(roadmapPath)) {
19
19
  const content = fs.readFileSync(roadmapPath, 'utf-8');
20
20
 
21
- // Find first incomplete phase
21
+ // Format 1: ## Phase N: Name [x] (heading format)
22
22
  const phaseMatches = content.matchAll(/##\s+Phase\s+(\d+)(?:\.(\d+))?[:\s]+(.+?)(?:\s*\[([x ])\])?$/gm);
23
23
  for (const match of phaseMatches) {
24
24
  const phaseNum = match[2] ? `${match[1]}.${match[2]}` : match[1];
@@ -31,16 +31,42 @@ function parsePlan(projectDir) {
31
31
  break;
32
32
  }
33
33
  }
34
+
35
+ // Format 2: Table format | 01 | [Name](link) | status | description |
36
+ if (!result.currentPhase) {
37
+ const tableMatches = content.matchAll(/\|\s*(\d+)\s*\|\s*\[([^\]]+)\][^\|]*\|\s*(\w+)\s*\|/g);
38
+ for (const match of tableMatches) {
39
+ const phaseNum = match[1].replace(/^0+/, '') || '0'; // strip leading zeros
40
+ const phaseName = match[2].trim();
41
+ const status = match[3].trim().toLowerCase();
42
+ const completed = status === 'complete' || status === 'done' || status === 'verified';
43
+
44
+ if (!completed) {
45
+ result.currentPhase = phaseNum;
46
+ result.currentPhaseName = phaseName;
47
+ break;
48
+ }
49
+ }
50
+ }
34
51
  }
35
52
 
36
53
  // Load current phase PLAN.md
37
54
  if (result.currentPhase) {
38
- const planPath = path.join(
39
- projectDir,
40
- '.planning',
41
- 'phases',
42
- `${result.currentPhase}-PLAN.md`
43
- );
55
+ const phasesDir = path.join(projectDir, '.planning', 'phases');
56
+ let planPath = path.join(phasesDir, `${result.currentPhase}-PLAN.md`);
57
+
58
+ // Try exact match first, then glob for prefixed names like "06-name-PLAN.md"
59
+ if (!fs.existsSync(planPath) && fs.existsSync(phasesDir)) {
60
+ const padded = result.currentPhase.toString().padStart(2, '0');
61
+ const files = fs.readdirSync(phasesDir);
62
+ const match = files.find(f =>
63
+ (f.startsWith(`${padded}-`) || f.startsWith(`${result.currentPhase}-`)) &&
64
+ f.endsWith('-PLAN.md')
65
+ );
66
+ if (match) {
67
+ planPath = path.join(phasesDir, match);
68
+ }
69
+ }
44
70
 
45
71
  if (fs.existsSync(planPath)) {
46
72
  const content = fs.readFileSync(planPath, 'utf-8');
@@ -10,6 +10,33 @@ function detectProject(projectDir) {
10
10
  if (fs.existsSync(tlcConfigPath)) {
11
11
  try {
12
12
  const config = JSON.parse(fs.readFileSync(tlcConfigPath, 'utf-8'));
13
+
14
+ // Check devServer config (Docker-managed apps)
15
+ if (config.devServer) {
16
+ if (config.devServer.type === 'docker') {
17
+ return {
18
+ name: 'Docker-managed (' + (config.devServer.containerName || 'docker') + ')',
19
+ type: 'docker',
20
+ cmd: null,
21
+ args: [],
22
+ port: config.devServer.port || config.devServer.internalPort || 5000,
23
+ healthCheck: config.devServer.healthCheck || null,
24
+ url: config.devServer.url || null
25
+ };
26
+ }
27
+ // Non-docker devServer with explicit start command
28
+ if (config.devServer.startCommand) {
29
+ const parts = config.devServer.startCommand.split(' ');
30
+ return {
31
+ name: 'Custom (.tlc.json devServer)',
32
+ cmd: parts[0],
33
+ args: parts.slice(1),
34
+ port: config.devServer.port || 3000
35
+ };
36
+ }
37
+ }
38
+
39
+ // Legacy: check server config
13
40
  if (config.server?.startCommand) {
14
41
  const parts = config.server.startCommand.split(' ');
15
42
  return {