tlc-claude-code 0.6.4 → 0.7.1
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 +164 -121
- package/autofix.md +327 -0
- package/bin/install.js +23 -2
- 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.md
ADDED
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
# /tlc:server - TLC Development Server
|
|
2
|
+
|
|
3
|
+
Launch a unified development environment with live app preview, real-time logs, and team collaboration.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
/tlc:server [--port 3147]
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What This Does
|
|
12
|
+
|
|
13
|
+
Starts a **mini-Replit experience** for your TLC project:
|
|
14
|
+
|
|
15
|
+
1. **Runs your app** - Auto-detects and starts your dev server
|
|
16
|
+
2. **Live preview** - Embeds running app in dashboard
|
|
17
|
+
3. **Real-time logs** - App logs, test output, git activity
|
|
18
|
+
4. **Team tools** - Task board, bug submission, status
|
|
19
|
+
5. **Hot reload** - Changes reflect immediately
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
23
|
+
│ TLC Development Server [Stop] [⚙] │
|
|
24
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
25
|
+
│ │ │
|
|
26
|
+
│ ┌─────────────────┐ │ ┌────────────────────────────┐ │
|
|
27
|
+
│ │ LIVE PREVIEW │ │ │ LOGS │ │
|
|
28
|
+
│ │ │ │ │ │ │
|
|
29
|
+
│ │ ┌───────────┐ │ │ │ [App] [Tests] [Git] │ │
|
|
30
|
+
│ │ │ Your App │ │ │ │ │ │
|
|
31
|
+
│ │ │ Running │ │ │ │ > Server started on :3000 │ │
|
|
32
|
+
│ │ │ Here │ │ │ │ > GET /api/users 200 12ms │ │
|
|
33
|
+
│ │ │ │ │ │ │ > POST /api/login 401 8ms │ │
|
|
34
|
+
│ │ └───────────┘ │ │ │ > ✓ 12 tests passing │ │
|
|
35
|
+
│ │ │ │ │ │ │
|
|
36
|
+
│ │ [Open in Tab] │ │ └────────────────────────────┘ │
|
|
37
|
+
│ └─────────────────┘ │ │
|
|
38
|
+
│ │ ┌────────────────────────────┐ │
|
|
39
|
+
├──────────────────────────┤ │ TASKS Phase 2 │ │
|
|
40
|
+
│ REPORT BUG │ │ │ │
|
|
41
|
+
│ ──────────────────── │ │ ✓ Task 1 @alice │ │
|
|
42
|
+
│ What's wrong? │ │ → Task 2 @bob (working) │ │
|
|
43
|
+
│ ┌────────────────────┐ │ │ ○ Task 3 (available) │ │
|
|
44
|
+
│ │ │ │ │ ○ Task 4 (available) │ │
|
|
45
|
+
│ └────────────────────┘ │ │ │ │
|
|
46
|
+
│ [Screenshot] [Submit] │ │ Tests: 12/15 passing │ │
|
|
47
|
+
│ │ └────────────────────────────┘ │
|
|
48
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Process
|
|
52
|
+
|
|
53
|
+
### Step 1: Detect Project Type
|
|
54
|
+
|
|
55
|
+
Scan for project configuration:
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
const PROJECT_TYPES = {
|
|
59
|
+
// Node.js / JavaScript
|
|
60
|
+
'package.json': {
|
|
61
|
+
detect: (pkg) => {
|
|
62
|
+
if (pkg.scripts?.dev) return { cmd: 'npm', args: ['run', 'dev'] };
|
|
63
|
+
if (pkg.scripts?.start) return { cmd: 'npm', args: ['start'] };
|
|
64
|
+
if (pkg.dependencies?.next) return { cmd: 'npx', args: ['next', 'dev'] };
|
|
65
|
+
if (pkg.dependencies?.vite) return { cmd: 'npx', args: ['vite'] };
|
|
66
|
+
if (pkg.dependencies?.express) return { cmd: 'node', args: ['src/index.js'] };
|
|
67
|
+
return { cmd: 'npm', args: ['start'] };
|
|
68
|
+
},
|
|
69
|
+
defaultPort: 3000
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
// Python
|
|
73
|
+
'pyproject.toml': {
|
|
74
|
+
detect: (toml) => {
|
|
75
|
+
if (toml.tool?.poetry) return { cmd: 'poetry', args: ['run', 'python', '-m', 'uvicorn', 'main:app', '--reload'] };
|
|
76
|
+
return { cmd: 'python', args: ['-m', 'uvicorn', 'main:app', '--reload'] };
|
|
77
|
+
},
|
|
78
|
+
defaultPort: 8000
|
|
79
|
+
},
|
|
80
|
+
'requirements.txt': {
|
|
81
|
+
cmd: 'python', args: ['-m', 'flask', 'run'],
|
|
82
|
+
defaultPort: 5000
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// Go
|
|
86
|
+
'go.mod': {
|
|
87
|
+
cmd: 'go', args: ['run', '.'],
|
|
88
|
+
defaultPort: 8080
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
// Ruby
|
|
92
|
+
'Gemfile': {
|
|
93
|
+
detect: (gemfile) => {
|
|
94
|
+
if (gemfile.includes('rails')) return { cmd: 'rails', args: ['server'] };
|
|
95
|
+
return { cmd: 'ruby', args: ['app.rb'] };
|
|
96
|
+
},
|
|
97
|
+
defaultPort: 3000
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
// Rust
|
|
101
|
+
'Cargo.toml': {
|
|
102
|
+
cmd: 'cargo', args: ['run'],
|
|
103
|
+
defaultPort: 8080
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Step 2: Start App Server
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
const { spawn } = require('child_process');
|
|
112
|
+
|
|
113
|
+
function startAppServer(projectType) {
|
|
114
|
+
const config = PROJECT_TYPES[projectType];
|
|
115
|
+
const appPort = process.env.PORT || config.defaultPort;
|
|
116
|
+
|
|
117
|
+
const app = spawn(config.cmd, config.args, {
|
|
118
|
+
env: { ...process.env, PORT: appPort },
|
|
119
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Stream stdout to dashboard
|
|
123
|
+
app.stdout.on('data', (data) => {
|
|
124
|
+
broadcast('app-log', { stream: 'stdout', data: data.toString() });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Stream stderr to dashboard
|
|
128
|
+
app.stderr.on('data', (data) => {
|
|
129
|
+
broadcast('app-log', { stream: 'stderr', data: data.toString() });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return { process: app, port: appPort };
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Step 3: Start TLC Dashboard Server
|
|
137
|
+
|
|
138
|
+
```javascript
|
|
139
|
+
const express = require('express');
|
|
140
|
+
const { WebSocketServer } = require('ws');
|
|
141
|
+
const { createProxyMiddleware } = require('http-proxy-middleware');
|
|
142
|
+
|
|
143
|
+
const app = express();
|
|
144
|
+
const TLC_PORT = 3147;
|
|
145
|
+
const APP_PORT = 3000; // detected from project
|
|
146
|
+
|
|
147
|
+
// Serve dashboard UI
|
|
148
|
+
app.use(express.static('.tlc/dashboard'));
|
|
149
|
+
app.use(express.json());
|
|
150
|
+
|
|
151
|
+
// Proxy to running app (for iframe embed)
|
|
152
|
+
app.use('/app', createProxyMiddleware({
|
|
153
|
+
target: `http://localhost:${APP_PORT}`,
|
|
154
|
+
changeOrigin: true,
|
|
155
|
+
pathRewrite: { '^/app': '' },
|
|
156
|
+
ws: true // WebSocket support for hot reload
|
|
157
|
+
}));
|
|
158
|
+
|
|
159
|
+
// API endpoints
|
|
160
|
+
app.get('/api/status', (req, res) => { /* ... */ });
|
|
161
|
+
app.get('/api/logs', (req, res) => { /* ... */ });
|
|
162
|
+
app.post('/api/bug', (req, res) => { /* ... */ });
|
|
163
|
+
app.get('/api/tasks', (req, res) => { /* ... */ });
|
|
164
|
+
app.post('/api/test', (req, res) => { runTests(); });
|
|
165
|
+
app.post('/api/restart', (req, res) => { restartApp(); });
|
|
166
|
+
|
|
167
|
+
const server = app.listen(TLC_PORT);
|
|
168
|
+
const wss = new WebSocketServer({ server });
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Step 4: Create Dashboard UI
|
|
172
|
+
|
|
173
|
+
Create `.tlc/dashboard/index.html`:
|
|
174
|
+
|
|
175
|
+
```html
|
|
176
|
+
<!DOCTYPE html>
|
|
177
|
+
<html>
|
|
178
|
+
<head>
|
|
179
|
+
<title>TLC Dev Server</title>
|
|
180
|
+
<style>
|
|
181
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
182
|
+
body {
|
|
183
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
184
|
+
background: #0d1117;
|
|
185
|
+
color: #e6edf3;
|
|
186
|
+
height: 100vh;
|
|
187
|
+
overflow: hidden;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.header {
|
|
191
|
+
background: #161b22;
|
|
192
|
+
padding: 12px 20px;
|
|
193
|
+
display: flex;
|
|
194
|
+
justify-content: space-between;
|
|
195
|
+
align-items: center;
|
|
196
|
+
border-bottom: 1px solid #30363d;
|
|
197
|
+
}
|
|
198
|
+
.header h1 { font-size: 16px; color: #58a6ff; }
|
|
199
|
+
.header .status { display: flex; gap: 12px; align-items: center; }
|
|
200
|
+
.header .status .dot { width: 8px; height: 8px; border-radius: 50%; }
|
|
201
|
+
.header .status .dot.running { background: #3fb950; }
|
|
202
|
+
.header .status .dot.stopped { background: #f85149; }
|
|
203
|
+
|
|
204
|
+
.main {
|
|
205
|
+
display: grid;
|
|
206
|
+
grid-template-columns: 1fr 400px;
|
|
207
|
+
grid-template-rows: 1fr 1fr;
|
|
208
|
+
height: calc(100vh - 50px);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.preview {
|
|
212
|
+
grid-row: 1 / 3;
|
|
213
|
+
border-right: 1px solid #30363d;
|
|
214
|
+
display: flex;
|
|
215
|
+
flex-direction: column;
|
|
216
|
+
}
|
|
217
|
+
.preview-header {
|
|
218
|
+
padding: 8px 12px;
|
|
219
|
+
background: #161b22;
|
|
220
|
+
display: flex;
|
|
221
|
+
justify-content: space-between;
|
|
222
|
+
align-items: center;
|
|
223
|
+
border-bottom: 1px solid #30363d;
|
|
224
|
+
}
|
|
225
|
+
.preview-header input {
|
|
226
|
+
flex: 1;
|
|
227
|
+
margin: 0 10px;
|
|
228
|
+
padding: 6px 10px;
|
|
229
|
+
background: #0d1117;
|
|
230
|
+
border: 1px solid #30363d;
|
|
231
|
+
border-radius: 6px;
|
|
232
|
+
color: #e6edf3;
|
|
233
|
+
}
|
|
234
|
+
.preview iframe {
|
|
235
|
+
flex: 1;
|
|
236
|
+
border: none;
|
|
237
|
+
background: white;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.logs {
|
|
241
|
+
border-bottom: 1px solid #30363d;
|
|
242
|
+
display: flex;
|
|
243
|
+
flex-direction: column;
|
|
244
|
+
}
|
|
245
|
+
.logs-header {
|
|
246
|
+
padding: 8px 12px;
|
|
247
|
+
background: #161b22;
|
|
248
|
+
display: flex;
|
|
249
|
+
gap: 8px;
|
|
250
|
+
border-bottom: 1px solid #30363d;
|
|
251
|
+
}
|
|
252
|
+
.logs-header button {
|
|
253
|
+
padding: 4px 12px;
|
|
254
|
+
background: transparent;
|
|
255
|
+
border: 1px solid #30363d;
|
|
256
|
+
border-radius: 4px;
|
|
257
|
+
color: #8b949e;
|
|
258
|
+
cursor: pointer;
|
|
259
|
+
}
|
|
260
|
+
.logs-header button.active {
|
|
261
|
+
background: #21262d;
|
|
262
|
+
color: #e6edf3;
|
|
263
|
+
border-color: #58a6ff;
|
|
264
|
+
}
|
|
265
|
+
.logs-content {
|
|
266
|
+
flex: 1;
|
|
267
|
+
overflow-y: auto;
|
|
268
|
+
padding: 10px;
|
|
269
|
+
font-family: monospace;
|
|
270
|
+
font-size: 12px;
|
|
271
|
+
background: #010409;
|
|
272
|
+
}
|
|
273
|
+
.log-line { padding: 2px 0; }
|
|
274
|
+
.log-line.error { color: #f85149; }
|
|
275
|
+
.log-line.success { color: #3fb950; }
|
|
276
|
+
.log-line.info { color: #58a6ff; }
|
|
277
|
+
.log-line.warn { color: #d29922; }
|
|
278
|
+
|
|
279
|
+
.sidebar {
|
|
280
|
+
display: flex;
|
|
281
|
+
flex-direction: column;
|
|
282
|
+
}
|
|
283
|
+
.tasks {
|
|
284
|
+
flex: 1;
|
|
285
|
+
overflow-y: auto;
|
|
286
|
+
padding: 12px;
|
|
287
|
+
}
|
|
288
|
+
.tasks h2 {
|
|
289
|
+
font-size: 12px;
|
|
290
|
+
text-transform: uppercase;
|
|
291
|
+
color: #8b949e;
|
|
292
|
+
margin-bottom: 12px;
|
|
293
|
+
}
|
|
294
|
+
.task {
|
|
295
|
+
padding: 10px;
|
|
296
|
+
background: #161b22;
|
|
297
|
+
border-radius: 6px;
|
|
298
|
+
margin-bottom: 8px;
|
|
299
|
+
border-left: 3px solid transparent;
|
|
300
|
+
}
|
|
301
|
+
.task.done { border-left-color: #3fb950; opacity: 0.7; }
|
|
302
|
+
.task.working { border-left-color: #58a6ff; }
|
|
303
|
+
.task.available { border-left-color: #8b949e; }
|
|
304
|
+
.task .title { font-weight: 500; }
|
|
305
|
+
.task .meta { font-size: 11px; color: #8b949e; margin-top: 4px; }
|
|
306
|
+
|
|
307
|
+
.bug-form {
|
|
308
|
+
padding: 12px;
|
|
309
|
+
background: #161b22;
|
|
310
|
+
border-top: 1px solid #30363d;
|
|
311
|
+
}
|
|
312
|
+
.bug-form h2 {
|
|
313
|
+
font-size: 12px;
|
|
314
|
+
text-transform: uppercase;
|
|
315
|
+
color: #8b949e;
|
|
316
|
+
margin-bottom: 12px;
|
|
317
|
+
}
|
|
318
|
+
.bug-form textarea {
|
|
319
|
+
width: 100%;
|
|
320
|
+
height: 60px;
|
|
321
|
+
padding: 8px;
|
|
322
|
+
background: #0d1117;
|
|
323
|
+
border: 1px solid #30363d;
|
|
324
|
+
border-radius: 6px;
|
|
325
|
+
color: #e6edf3;
|
|
326
|
+
resize: none;
|
|
327
|
+
margin-bottom: 8px;
|
|
328
|
+
}
|
|
329
|
+
.bug-form .actions {
|
|
330
|
+
display: flex;
|
|
331
|
+
gap: 8px;
|
|
332
|
+
}
|
|
333
|
+
.bug-form button {
|
|
334
|
+
padding: 8px 16px;
|
|
335
|
+
border-radius: 6px;
|
|
336
|
+
border: none;
|
|
337
|
+
cursor: pointer;
|
|
338
|
+
}
|
|
339
|
+
.bug-form .screenshot {
|
|
340
|
+
background: #21262d;
|
|
341
|
+
color: #e6edf3;
|
|
342
|
+
flex: 1;
|
|
343
|
+
}
|
|
344
|
+
.bug-form .submit {
|
|
345
|
+
background: #238636;
|
|
346
|
+
color: white;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.stats {
|
|
350
|
+
display: grid;
|
|
351
|
+
grid-template-columns: repeat(3, 1fr);
|
|
352
|
+
gap: 8px;
|
|
353
|
+
padding: 12px;
|
|
354
|
+
background: #161b22;
|
|
355
|
+
border-top: 1px solid #30363d;
|
|
356
|
+
}
|
|
357
|
+
.stat {
|
|
358
|
+
text-align: center;
|
|
359
|
+
padding: 8px;
|
|
360
|
+
background: #0d1117;
|
|
361
|
+
border-radius: 6px;
|
|
362
|
+
}
|
|
363
|
+
.stat-value { font-size: 20px; font-weight: bold; }
|
|
364
|
+
.stat-value.green { color: #3fb950; }
|
|
365
|
+
.stat-value.red { color: #f85149; }
|
|
366
|
+
.stat-value.blue { color: #58a6ff; }
|
|
367
|
+
.stat-label { font-size: 10px; color: #8b949e; }
|
|
368
|
+
</style>
|
|
369
|
+
</head>
|
|
370
|
+
<body>
|
|
371
|
+
<div class="header">
|
|
372
|
+
<h1>TLC Dev Server</h1>
|
|
373
|
+
<div class="status">
|
|
374
|
+
<span class="dot running" id="status-dot"></span>
|
|
375
|
+
<span id="status-text">Running</span>
|
|
376
|
+
<button onclick="runTests()">Run Tests</button>
|
|
377
|
+
<button onclick="restartApp()">Restart</button>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
<div class="main">
|
|
382
|
+
<div class="preview">
|
|
383
|
+
<div class="preview-header">
|
|
384
|
+
<span>Preview</span>
|
|
385
|
+
<input type="text" id="url-bar" value="http://localhost:3000/" onkeypress="if(event.key==='Enter')navigateTo(this.value)">
|
|
386
|
+
<button onclick="openInTab()">Open in Tab ↗</button>
|
|
387
|
+
</div>
|
|
388
|
+
<iframe id="app-frame" src="/app/"></iframe>
|
|
389
|
+
</div>
|
|
390
|
+
|
|
391
|
+
<div class="logs">
|
|
392
|
+
<div class="logs-header">
|
|
393
|
+
<button class="active" onclick="showLogs('app')">App</button>
|
|
394
|
+
<button onclick="showLogs('test')">Tests</button>
|
|
395
|
+
<button onclick="showLogs('git')">Git</button>
|
|
396
|
+
</div>
|
|
397
|
+
<div class="logs-content" id="logs"></div>
|
|
398
|
+
</div>
|
|
399
|
+
|
|
400
|
+
<div class="sidebar">
|
|
401
|
+
<div class="tasks" id="tasks">
|
|
402
|
+
<h2>Phase 2: Authentication</h2>
|
|
403
|
+
<!-- Tasks populated by JS -->
|
|
404
|
+
</div>
|
|
405
|
+
|
|
406
|
+
<div class="bug-form">
|
|
407
|
+
<h2>Report Bug</h2>
|
|
408
|
+
<textarea id="bug-desc" placeholder="What went wrong?"></textarea>
|
|
409
|
+
<div class="actions">
|
|
410
|
+
<button class="screenshot" onclick="takeScreenshot()">📷 Screenshot</button>
|
|
411
|
+
<button class="submit" onclick="submitBug()">Submit Bug</button>
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
|
|
415
|
+
<div class="stats">
|
|
416
|
+
<div class="stat">
|
|
417
|
+
<div class="stat-value green" id="tests-pass">12</div>
|
|
418
|
+
<div class="stat-label">Passing</div>
|
|
419
|
+
</div>
|
|
420
|
+
<div class="stat">
|
|
421
|
+
<div class="stat-value red" id="tests-fail">3</div>
|
|
422
|
+
<div class="stat-label">Failing</div>
|
|
423
|
+
</div>
|
|
424
|
+
<div class="stat">
|
|
425
|
+
<div class="stat-value blue" id="bugs-open">2</div>
|
|
426
|
+
<div class="stat-label">Open Bugs</div>
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
|
|
432
|
+
<script>
|
|
433
|
+
const ws = new WebSocket(`ws://${location.host}`);
|
|
434
|
+
const logs = { app: [], test: [], git: [] };
|
|
435
|
+
let currentLogType = 'app';
|
|
436
|
+
|
|
437
|
+
ws.onmessage = (event) => {
|
|
438
|
+
const msg = JSON.parse(event.data);
|
|
439
|
+
|
|
440
|
+
switch(msg.type) {
|
|
441
|
+
case 'app-log':
|
|
442
|
+
addLog('app', msg.data.data, detectLogLevel(msg.data.data));
|
|
443
|
+
break;
|
|
444
|
+
case 'test-output':
|
|
445
|
+
addLog('test', msg.data.data, msg.data.stream === 'stderr' ? 'error' : 'info');
|
|
446
|
+
break;
|
|
447
|
+
case 'test-complete':
|
|
448
|
+
addLog('test', `Tests completed: exit code ${msg.data.exitCode}`,
|
|
449
|
+
msg.data.exitCode === 0 ? 'success' : 'error');
|
|
450
|
+
refreshStats();
|
|
451
|
+
break;
|
|
452
|
+
case 'git-activity':
|
|
453
|
+
addLog('git', msg.data.entry, 'info');
|
|
454
|
+
break;
|
|
455
|
+
case 'app-restart':
|
|
456
|
+
addLog('app', '--- App restarting ---', 'warn');
|
|
457
|
+
break;
|
|
458
|
+
case 'task-update':
|
|
459
|
+
refreshTasks();
|
|
460
|
+
break;
|
|
461
|
+
case 'bug-created':
|
|
462
|
+
addLog('app', `Bug ${msg.data.bugId} created`, 'warn');
|
|
463
|
+
refreshStats();
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
function detectLogLevel(text) {
|
|
469
|
+
if (/error|fail|exception/i.test(text)) return 'error';
|
|
470
|
+
if (/warn/i.test(text)) return 'warn';
|
|
471
|
+
if (/success|✓|pass/i.test(text)) return 'success';
|
|
472
|
+
return '';
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function addLog(type, text, level = '') {
|
|
476
|
+
logs[type].push({ text, level, time: new Date() });
|
|
477
|
+
if (logs[type].length > 1000) logs[type].shift();
|
|
478
|
+
if (type === currentLogType) renderLogs();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function renderLogs() {
|
|
482
|
+
const container = document.getElementById('logs');
|
|
483
|
+
container.innerHTML = logs[currentLogType].map(l =>
|
|
484
|
+
`<div class="log-line ${l.level}">${l.text}</div>`
|
|
485
|
+
).join('');
|
|
486
|
+
container.scrollTop = container.scrollHeight;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function showLogs(type) {
|
|
490
|
+
currentLogType = type;
|
|
491
|
+
document.querySelectorAll('.logs-header button').forEach(b => b.classList.remove('active'));
|
|
492
|
+
event.target.classList.add('active');
|
|
493
|
+
renderLogs();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function navigateTo(url) {
|
|
497
|
+
document.getElementById('app-frame').src = url.replace(/^http:\/\/localhost:\d+/, '/app');
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function openInTab() {
|
|
501
|
+
window.open('http://localhost:3000', '_blank');
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function runTests() {
|
|
505
|
+
addLog('test', '--- Running tests ---', 'info');
|
|
506
|
+
await fetch('/api/test', { method: 'POST' });
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function restartApp() {
|
|
510
|
+
await fetch('/api/restart', { method: 'POST' });
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async function submitBug() {
|
|
514
|
+
const desc = document.getElementById('bug-desc').value;
|
|
515
|
+
if (!desc) return alert('Please describe the bug');
|
|
516
|
+
|
|
517
|
+
const res = await fetch('/api/bug', {
|
|
518
|
+
method: 'POST',
|
|
519
|
+
headers: { 'Content-Type': 'application/json' },
|
|
520
|
+
body: JSON.stringify({
|
|
521
|
+
description: desc,
|
|
522
|
+
url: document.getElementById('url-bar').value,
|
|
523
|
+
screenshot: window.lastScreenshot || null
|
|
524
|
+
})
|
|
525
|
+
});
|
|
526
|
+
const data = await res.json();
|
|
527
|
+
alert(`Bug ${data.bugId} created!`);
|
|
528
|
+
document.getElementById('bug-desc').value = '';
|
|
529
|
+
window.lastScreenshot = null;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function takeScreenshot() {
|
|
533
|
+
// Use html2canvas or similar to capture the iframe
|
|
534
|
+
// For now, just note that a screenshot was requested
|
|
535
|
+
alert('Screenshot captured (preview area)');
|
|
536
|
+
window.lastScreenshot = 'screenshot-placeholder';
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async function refreshTasks() {
|
|
540
|
+
const res = await fetch('/api/tasks');
|
|
541
|
+
const tasks = await res.json();
|
|
542
|
+
const container = document.getElementById('tasks');
|
|
543
|
+
container.innerHTML = `<h2>Phase ${tasks.phase}: ${tasks.phaseName}</h2>` +
|
|
544
|
+
tasks.items.map(t => `
|
|
545
|
+
<div class="task ${t.status}">
|
|
546
|
+
<div class="title">${t.status === 'done' ? '✓' : t.status === 'working' ? '→' : '○'} ${t.title}</div>
|
|
547
|
+
<div class="meta">${t.owner ? '@' + t.owner : 'Available'}</div>
|
|
548
|
+
</div>
|
|
549
|
+
`).join('');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async function refreshStats() {
|
|
553
|
+
const res = await fetch('/api/status');
|
|
554
|
+
const data = await res.json();
|
|
555
|
+
document.getElementById('tests-pass').textContent = data.testsPass || 0;
|
|
556
|
+
document.getElementById('tests-fail').textContent = data.testsFail || 0;
|
|
557
|
+
document.getElementById('bugs-open').textContent = data.bugsOpen || 0;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Initial load
|
|
561
|
+
refreshTasks();
|
|
562
|
+
refreshStats();
|
|
563
|
+
addLog('app', 'Connected to TLC Dev Server', 'success');
|
|
564
|
+
</script>
|
|
565
|
+
</body>
|
|
566
|
+
</html>
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### Step 5: File Watching & Hot Reload
|
|
570
|
+
|
|
571
|
+
```javascript
|
|
572
|
+
const chokidar = require('chokidar');
|
|
573
|
+
|
|
574
|
+
// Watch source files for changes
|
|
575
|
+
const watcher = chokidar.watch(['src', 'lib', 'app'], {
|
|
576
|
+
ignored: /node_modules/,
|
|
577
|
+
persistent: true
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
watcher.on('change', (path) => {
|
|
581
|
+
broadcast('file-change', { path });
|
|
582
|
+
|
|
583
|
+
// Many frameworks auto-reload, but notify dashboard
|
|
584
|
+
broadcast('app-log', {
|
|
585
|
+
stream: 'stdout',
|
|
586
|
+
data: `File changed: ${path}`
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
// Watch .planning for TLC updates
|
|
591
|
+
chokidar.watch('.planning', { persistent: true })
|
|
592
|
+
.on('change', (path) => {
|
|
593
|
+
broadcast('tlc-update', { path });
|
|
594
|
+
if (path.includes('PLAN.md')) {
|
|
595
|
+
broadcast('task-update', {});
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
### Step 6: Screenshot Capture
|
|
601
|
+
|
|
602
|
+
For QA bug reports with screenshots:
|
|
603
|
+
|
|
604
|
+
```javascript
|
|
605
|
+
// Client-side: Capture iframe content
|
|
606
|
+
async function capturePreview() {
|
|
607
|
+
const iframe = document.getElementById('app-frame');
|
|
608
|
+
|
|
609
|
+
// Option 1: Use html2canvas on iframe document
|
|
610
|
+
const canvas = await html2canvas(iframe.contentDocument.body);
|
|
611
|
+
return canvas.toDataURL('image/png');
|
|
612
|
+
|
|
613
|
+
// Option 2: Use browser screenshot API (if available)
|
|
614
|
+
// Option 3: Server-side puppeteer capture
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Server-side: Store screenshot with bug
|
|
618
|
+
app.post('/api/bug', async (req, res) => {
|
|
619
|
+
const { description, url, screenshot } = req.body;
|
|
620
|
+
|
|
621
|
+
// Save screenshot to .tlc/screenshots/BUG-XXX.png
|
|
622
|
+
if (screenshot) {
|
|
623
|
+
const screenshotPath = `.tlc/screenshots/${bugId}.png`;
|
|
624
|
+
fs.writeFileSync(screenshotPath, Buffer.from(screenshot.split(',')[1], 'base64'));
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Create bug entry with screenshot reference
|
|
628
|
+
const bugEntry = createBugEntry({
|
|
629
|
+
description,
|
|
630
|
+
url,
|
|
631
|
+
screenshot: screenshot ? `screenshots/${bugId}.png` : null
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
res.json({ success: true, bugId });
|
|
635
|
+
});
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
## Configuration
|
|
639
|
+
|
|
640
|
+
In `.tlc.json`:
|
|
641
|
+
|
|
642
|
+
```json
|
|
643
|
+
{
|
|
644
|
+
"server": {
|
|
645
|
+
"dashboardPort": 3147,
|
|
646
|
+
"appPort": 3000,
|
|
647
|
+
"openBrowser": true,
|
|
648
|
+
"proxy": {
|
|
649
|
+
"enabled": true,
|
|
650
|
+
"pathRewrite": { "^/app": "" }
|
|
651
|
+
},
|
|
652
|
+
"watch": {
|
|
653
|
+
"source": ["src", "lib", "app"],
|
|
654
|
+
"ignore": ["node_modules", "dist", ".git"]
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
## Custom Start Commands
|
|
661
|
+
|
|
662
|
+
Override auto-detection in `.tlc.json`:
|
|
663
|
+
|
|
664
|
+
```json
|
|
665
|
+
{
|
|
666
|
+
"server": {
|
|
667
|
+
"startCommand": "npm run dev:custom",
|
|
668
|
+
"appPort": 4000
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
## Example Session
|
|
674
|
+
|
|
675
|
+
```
|
|
676
|
+
> /tlc:server
|
|
677
|
+
|
|
678
|
+
Detecting project type...
|
|
679
|
+
Found: package.json (Next.js)
|
|
680
|
+
Start command: npm run dev
|
|
681
|
+
App port: 3000
|
|
682
|
+
|
|
683
|
+
Starting app server...
|
|
684
|
+
✓ App running at http://localhost:3000
|
|
685
|
+
|
|
686
|
+
Starting TLC dashboard...
|
|
687
|
+
✓ Dashboard at http://localhost:3147
|
|
688
|
+
|
|
689
|
+
Opening browser...
|
|
690
|
+
|
|
691
|
+
╭──────────────────────────────────────────────────────────────╮
|
|
692
|
+
│ TLC Dev Server │
|
|
693
|
+
│ │
|
|
694
|
+
│ Dashboard: http://localhost:3147 │
|
|
695
|
+
│ App: http://localhost:3000 (embedded in dashboard) │
|
|
696
|
+
│ │
|
|
697
|
+
│ Share with QA: http://192.168.1.5:3147 │
|
|
698
|
+
╰──────────────────────────────────────────────────────────────╯
|
|
699
|
+
|
|
700
|
+
[10:15:32] App started
|
|
701
|
+
[10:15:33] Client connected (Chrome)
|
|
702
|
+
[10:15:45] GET /api/users 200 12ms
|
|
703
|
+
[10:16:02] File changed: src/components/Login.tsx
|
|
704
|
+
[10:16:03] Hot reload triggered
|
|
705
|
+
[10:16:30] Bug submitted: BUG-009 by QA
|
|
706
|
+
[10:17:00] Tests started
|
|
707
|
+
[10:17:15] Tests complete: 14 pass, 1 fail
|
|
708
|
+
|
|
709
|
+
Press Ctrl+C to stop
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
## Features Summary
|
|
713
|
+
|
|
714
|
+
| Feature | Description |
|
|
715
|
+
|---------|-------------|
|
|
716
|
+
| **Live Preview** | Your app embedded in dashboard |
|
|
717
|
+
| **App Logs** | Real-time stdout/stderr from your app |
|
|
718
|
+
| **Test Logs** | Test output with pass/fail highlighting |
|
|
719
|
+
| **Git Activity** | Commits, pulls, pushes as they happen |
|
|
720
|
+
| **Task Board** | Current phase tasks with claim status |
|
|
721
|
+
| **Bug Submission** | Form with optional screenshot |
|
|
722
|
+
| **Hot Reload** | File changes trigger app refresh |
|
|
723
|
+
| **Proxy** | Dashboard proxies to app (avoids CORS) |
|
|
724
|
+
|
|
725
|
+
## QA Workflow
|
|
726
|
+
|
|
727
|
+
1. Engineer runs `/tlc:server`
|
|
728
|
+
2. QA opens `http://192.168.1.x:3147` in browser
|
|
729
|
+
3. QA sees live preview of app + task board
|
|
730
|
+
4. QA tests features in the embedded preview
|
|
731
|
+
5. QA finds bug → fills form → attaches screenshot
|
|
732
|
+
6. Bug appears in `.planning/BUGS.md` instantly
|
|
733
|
+
7. Engineer sees bug in logs, fixes it
|
|
734
|
+
8. App hot-reloads, QA re-tests
|
|
735
|
+
9. QA marks bug verified
|
|
736
|
+
|
|
737
|
+
## Notes
|
|
738
|
+
|
|
739
|
+
- Single URL for everything (PO/QA don't need technical setup)
|
|
740
|
+
- Works on local network (same WiFi)
|
|
741
|
+
- For remote access, use ngrok: `ngrok http 3147`
|
|
742
|
+
- All data flows through git (bugs, tasks, status)
|