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/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)