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 +1 -1
- package/server/index.js +33 -7
- package/server/lib/plan-parser.js +33 -7
- package/server/lib/project-detector.js +27 -0
package/package.json
CHANGED
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
|
-
|
|
639
|
-
|
|
640
|
-
const
|
|
641
|
-
|
|
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
|
-
//
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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 {
|