tlc-claude-code 0.6.1 → 0.6.3

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.
Files changed (42) hide show
  1. package/PROJECT.md +46 -0
  2. package/README.md +6 -0
  3. package/bin/install.js +1 -0
  4. package/dashboard/dist/App.d.ts +5 -0
  5. package/dashboard/dist/App.js +49 -0
  6. package/dashboard/dist/App.test.d.ts +1 -0
  7. package/dashboard/dist/App.test.js +137 -0
  8. package/dashboard/dist/components/AgentsPane.d.ts +18 -0
  9. package/dashboard/dist/components/AgentsPane.js +77 -0
  10. package/dashboard/dist/components/AgentsPane.test.d.ts +1 -0
  11. package/dashboard/dist/components/AgentsPane.test.js +52 -0
  12. package/dashboard/dist/components/ChatPane.d.ts +6 -0
  13. package/dashboard/dist/components/ChatPane.js +34 -0
  14. package/dashboard/dist/components/ChatPane.test.d.ts +1 -0
  15. package/dashboard/dist/components/ChatPane.test.js +36 -0
  16. package/dashboard/dist/components/GitHubPane.d.ts +16 -0
  17. package/dashboard/dist/components/GitHubPane.js +121 -0
  18. package/dashboard/dist/components/GitHubPane.test.d.ts +1 -0
  19. package/dashboard/dist/components/GitHubPane.test.js +79 -0
  20. package/dashboard/dist/components/PhasesPane.d.ts +8 -0
  21. package/dashboard/dist/components/PhasesPane.js +65 -0
  22. package/dashboard/dist/components/PhasesPane.test.d.ts +1 -0
  23. package/dashboard/dist/components/PhasesPane.test.js +119 -0
  24. package/dashboard/dist/components/PlanSync.d.ts +13 -0
  25. package/dashboard/dist/components/PlanSync.js +89 -0
  26. package/dashboard/dist/components/PlanSync.test.d.ts +1 -0
  27. package/dashboard/dist/components/PlanSync.test.js +117 -0
  28. package/dashboard/dist/components/PreviewPane.d.ts +6 -0
  29. package/dashboard/dist/components/PreviewPane.js +43 -0
  30. package/dashboard/dist/components/PreviewPane.test.d.ts +1 -0
  31. package/dashboard/dist/components/PreviewPane.test.js +36 -0
  32. package/dashboard/dist/components/StatusPane.d.ts +1 -0
  33. package/dashboard/dist/components/StatusPane.js +32 -0
  34. package/dashboard/dist/components/StatusPane.test.d.ts +1 -0
  35. package/dashboard/dist/components/StatusPane.test.js +51 -0
  36. package/dashboard/dist/index.d.ts +2 -0
  37. package/dashboard/dist/index.js +13 -0
  38. package/dashboard/package.json +34 -0
  39. package/help.md +99 -115
  40. package/import-project.md +246 -0
  41. package/package.json +9 -2
  42. package/tlc.md +197 -217
@@ -0,0 +1,79 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+ import { render } from 'ink-testing-library';
4
+ import { GitHubPane } from './GitHubPane.js';
5
+ // Mock child_process to avoid actual gh calls
6
+ vi.mock('child_process', () => ({
7
+ exec: vi.fn((cmd, opts, cb) => {
8
+ if (typeof opts === 'function') {
9
+ cb = opts;
10
+ }
11
+ // Simulate gh CLI not available by default
12
+ const error = new Error('gh not found');
13
+ if (cb)
14
+ cb(error, '', '');
15
+ return { stdout: null, stderr: null };
16
+ }),
17
+ }));
18
+ vi.mock('util', async () => {
19
+ const actual = await vi.importActual('util');
20
+ return {
21
+ ...actual,
22
+ promisify: (fn) => async (...args) => {
23
+ return new Promise((resolve, reject) => {
24
+ fn(...args, (err, stdout, stderr) => {
25
+ if (err)
26
+ reject(err);
27
+ else
28
+ resolve({ stdout, stderr });
29
+ });
30
+ });
31
+ },
32
+ };
33
+ });
34
+ describe('GitHubPane', () => {
35
+ beforeEach(() => {
36
+ vi.clearAllMocks();
37
+ });
38
+ describe('loading state', () => {
39
+ it('shows loading message initially', () => {
40
+ const { lastFrame } = render(_jsx(GitHubPane, { isActive: false }));
41
+ const output = lastFrame();
42
+ expect(output).toContain('Loading issues...');
43
+ });
44
+ });
45
+ describe('error state', () => {
46
+ it('shows error when gh CLI fails', async () => {
47
+ const { lastFrame } = render(_jsx(GitHubPane, { isActive: false }));
48
+ // Wait for effect to run
49
+ await new Promise(resolve => setTimeout(resolve, 100));
50
+ const output = lastFrame();
51
+ expect(output).toContain('gh CLI not available');
52
+ });
53
+ it('shows install hint on error', async () => {
54
+ const { lastFrame } = render(_jsx(GitHubPane, { isActive: false }));
55
+ await new Promise(resolve => setTimeout(resolve, 100));
56
+ const output = lastFrame();
57
+ expect(output).toContain('Make sure gh CLI is installed');
58
+ });
59
+ });
60
+ describe('controls', () => {
61
+ it('shows controls when active and has issues', async () => {
62
+ // This test would need proper mocking of successful gh response
63
+ // For now, we test that the component doesn't crash
64
+ const { lastFrame } = render(_jsx(GitHubPane, { isActive: true }));
65
+ await new Promise(resolve => setTimeout(resolve, 100));
66
+ const output = lastFrame();
67
+ // Either shows controls or error state
68
+ expect(output).toBeDefined();
69
+ });
70
+ });
71
+ describe('callback', () => {
72
+ it('accepts onAssignToAgent callback', () => {
73
+ const mockCallback = vi.fn();
74
+ const { lastFrame } = render(_jsx(GitHubPane, { isActive: true, onAssignToAgent: mockCallback }));
75
+ // Component should render without error
76
+ expect(lastFrame()).toBeDefined();
77
+ });
78
+ });
79
+ });
@@ -0,0 +1,8 @@
1
+ interface Phase {
2
+ number: number;
3
+ name: string;
4
+ status: 'completed' | 'in_progress' | 'pending';
5
+ }
6
+ export declare function PhasesPane(): import("react/jsx-runtime").JSX.Element;
7
+ export declare function parseRoadmap(content: string): Phase[];
8
+ export {};
@@ -0,0 +1,65 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { useState, useEffect } from 'react';
4
+ import { readFile } from 'fs/promises';
5
+ import { existsSync } from 'fs';
6
+ import { join } from 'path';
7
+ export function PhasesPane() {
8
+ const [phases, setPhases] = useState([]);
9
+ const [loading, setLoading] = useState(true);
10
+ useEffect(() => {
11
+ async function loadPhases() {
12
+ const roadmapPath = join(process.cwd(), '.planning', 'ROADMAP.md');
13
+ if (!existsSync(roadmapPath)) {
14
+ setPhases([]);
15
+ setLoading(false);
16
+ return;
17
+ }
18
+ try {
19
+ const content = await readFile(roadmapPath, 'utf-8');
20
+ const parsed = parseRoadmap(content);
21
+ setPhases(parsed);
22
+ }
23
+ catch (e) {
24
+ setPhases([]);
25
+ }
26
+ setLoading(false);
27
+ }
28
+ loadPhases();
29
+ const interval = setInterval(loadPhases, 5000);
30
+ return () => clearInterval(interval);
31
+ }, []);
32
+ if (loading) {
33
+ return (_jsx(Box, { padding: 1, children: _jsx(Text, { color: "gray", children: "Loading..." }) }));
34
+ }
35
+ if (phases.length === 0) {
36
+ return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", children: "No roadmap found." }), _jsx(Text, { color: "gray", dimColor: true, children: "Run /tdd:new-project or /tdd:init" })] }));
37
+ }
38
+ return (_jsx(Box, { padding: 1, flexDirection: "column", children: phases.map((phase) => (_jsxs(Box, { children: [_jsx(Text, { color: phase.status === 'completed' ? 'green' :
39
+ phase.status === 'in_progress' ? 'yellow' :
40
+ 'gray', children: phase.status === 'completed' ? ' [x] ' :
41
+ phase.status === 'in_progress' ? ' [>] ' :
42
+ ' [ ] ' }), _jsxs(Text, { color: phase.status === 'in_progress' ? 'cyan' : 'white', children: [phase.number, ". ", phase.name] })] }, phase.number))) }));
43
+ }
44
+ export function parseRoadmap(content) {
45
+ const phases = [];
46
+ const lines = content.split('\n');
47
+ for (const line of lines) {
48
+ // Match patterns like "## Phase 1: Setup" or "### 1. Auth System"
49
+ const match = line.match(/^#+\s*(?:Phase\s+)?(\d+)[.:]?\s*(.+)/i);
50
+ if (match) {
51
+ const num = parseInt(match[1], 10);
52
+ const name = match[2].replace(/\[.*?\]/g, '').trim();
53
+ // Determine status from markers
54
+ let status = 'pending';
55
+ if (line.includes('[x]') || line.includes('[completed]')) {
56
+ status = 'completed';
57
+ }
58
+ else if (line.includes('[>]') || line.includes('[in progress]') || line.includes('[current]')) {
59
+ status = 'in_progress';
60
+ }
61
+ phases.push({ number: num, name, status });
62
+ }
63
+ }
64
+ return phases;
65
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,119 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+ import { render } from 'ink-testing-library';
4
+ import { PhasesPane, parseRoadmap } from './PhasesPane.js';
5
+ import { vol } from 'memfs';
6
+ // Mock fs modules
7
+ vi.mock('fs', async () => {
8
+ const memfs = await import('memfs');
9
+ return memfs.fs;
10
+ });
11
+ vi.mock('fs/promises', async () => {
12
+ const memfs = await import('memfs');
13
+ return memfs.fs.promises;
14
+ });
15
+ describe('PhasesPane', () => {
16
+ beforeEach(() => {
17
+ vol.reset();
18
+ });
19
+ describe('parseRoadmap', () => {
20
+ it('parses "## Phase N: Name" format', () => {
21
+ const content = `# Roadmap
22
+
23
+ ## Phase 1: Setup
24
+ Initial setup
25
+
26
+ ## Phase 2: Core Features
27
+ Build the main stuff`;
28
+ const phases = parseRoadmap(content);
29
+ expect(phases).toHaveLength(2);
30
+ expect(phases[0]).toEqual({ number: 1, name: 'Setup', status: 'pending' });
31
+ expect(phases[1]).toEqual({ number: 2, name: 'Core Features', status: 'pending' });
32
+ });
33
+ it('parses "### N. Name" format', () => {
34
+ const content = `# Roadmap
35
+
36
+ ### 1. Auth System
37
+ ### 2. User Dashboard
38
+ ### 3. Reports`;
39
+ const phases = parseRoadmap(content);
40
+ expect(phases).toHaveLength(3);
41
+ expect(phases[0].name).toBe('Auth System');
42
+ expect(phases[2].number).toBe(3);
43
+ });
44
+ it('detects completed status from [x]', () => {
45
+ const content = `## Phase 1: Setup [x]
46
+ ## Phase 2: Build`;
47
+ const phases = parseRoadmap(content);
48
+ expect(phases[0].status).toBe('completed');
49
+ expect(phases[1].status).toBe('pending');
50
+ });
51
+ it('detects completed status from [completed]', () => {
52
+ const content = `## Phase 1: Setup [completed]`;
53
+ const phases = parseRoadmap(content);
54
+ expect(phases[0].status).toBe('completed');
55
+ });
56
+ it('detects in_progress status from [>]', () => {
57
+ const content = `## Phase 1: Setup [x]
58
+ ## Phase 2: Build [>]
59
+ ## Phase 3: Deploy`;
60
+ const phases = parseRoadmap(content);
61
+ expect(phases[0].status).toBe('completed');
62
+ expect(phases[1].status).toBe('in_progress');
63
+ expect(phases[2].status).toBe('pending');
64
+ });
65
+ it('detects in_progress from [in progress]', () => {
66
+ const content = `## Phase 1: Build [in progress]`;
67
+ const phases = parseRoadmap(content);
68
+ expect(phases[0].status).toBe('in_progress');
69
+ });
70
+ it('detects in_progress from [current]', () => {
71
+ const content = `## Phase 1: Build [current]`;
72
+ const phases = parseRoadmap(content);
73
+ expect(phases[0].status).toBe('in_progress');
74
+ });
75
+ it('returns empty array for content with no phases', () => {
76
+ const content = `# Just a readme
77
+
78
+ Some text without phases.`;
79
+ const phases = parseRoadmap(content);
80
+ expect(phases).toEqual([]);
81
+ });
82
+ it('strips status markers from name', () => {
83
+ const content = `## Phase 1: Setup [completed]`;
84
+ const phases = parseRoadmap(content);
85
+ expect(phases[0].name).toBe('Setup');
86
+ });
87
+ });
88
+ describe('component rendering', () => {
89
+ it('shows loading initially', () => {
90
+ const { lastFrame } = render(_jsx(PhasesPane, {}));
91
+ expect(lastFrame()).toContain('Loading...');
92
+ });
93
+ it('shows no roadmap message when file missing', async () => {
94
+ const { lastFrame } = render(_jsx(PhasesPane, {}));
95
+ await new Promise(resolve => setTimeout(resolve, 100));
96
+ const output = lastFrame();
97
+ expect(output).toContain('No roadmap found');
98
+ expect(output).toContain('/tdd:new-project');
99
+ });
100
+ it('renders phases from roadmap file', async () => {
101
+ // Create mock roadmap file
102
+ vol.fromJSON({
103
+ [process.cwd() + '/.planning/ROADMAP.md']: `# Roadmap
104
+
105
+ ## Phase 1: Auth [x]
106
+ ## Phase 2: Dashboard [>]
107
+ ## Phase 3: Reports`
108
+ });
109
+ const { lastFrame } = render(_jsx(PhasesPane, {}));
110
+ await new Promise(resolve => setTimeout(resolve, 100));
111
+ const output = lastFrame();
112
+ expect(output).toContain('1. Auth');
113
+ expect(output).toContain('2. Dashboard');
114
+ expect(output).toContain('3. Reports');
115
+ expect(output).toContain('[x]');
116
+ expect(output).toContain('[>]');
117
+ });
118
+ });
119
+ });
@@ -0,0 +1,13 @@
1
+ interface Task {
2
+ id: string;
3
+ title: string;
4
+ description: string;
5
+ phase: number;
6
+ }
7
+ export declare function parseTasksFromPlan(planPath: string): Promise<Task[]>;
8
+ export declare function syncPlanToGitHub(tasks: Task[], phaseNumber: number, phaseName: string): Promise<Map<string, number>>;
9
+ export declare function markIssueInProgress(issueNumber: number): Promise<void>;
10
+ export declare function markIssueComplete(issueNumber: number): Promise<void>;
11
+ export declare function isPlanApproved(planPath: string): Promise<boolean>;
12
+ export declare function approvePlan(planPath: string): Promise<void>;
13
+ export {};
@@ -0,0 +1,89 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { readFile } from 'fs/promises';
4
+ import { existsSync } from 'fs';
5
+ const execAsync = promisify(exec);
6
+ // Parse tasks from a PLAN.md file
7
+ export async function parseTasksFromPlan(planPath) {
8
+ if (!existsSync(planPath))
9
+ return [];
10
+ const content = await readFile(planPath, 'utf-8');
11
+ const tasks = [];
12
+ // Match task blocks like:
13
+ // ## Task 1: Setup auth
14
+ // or <task id="01-01">
15
+ const taskRegex = /(?:##\s*Task\s*(\d+)[:\s]+(.+)|<task\s+id="([^"]+)"[^>]*>)/gi;
16
+ let match;
17
+ while ((match = taskRegex.exec(content)) !== null) {
18
+ const id = match[3] || `task-${match[1]}`;
19
+ const title = match[2] || id;
20
+ // Get description (text until next heading or task tag)
21
+ const startIdx = match.index + match[0].length;
22
+ const nextMatch = content.slice(startIdx).match(/(?:##\s*Task|<task\s|<\/task>)/);
23
+ const endIdx = nextMatch ? startIdx + nextMatch.index : startIdx + 500;
24
+ const description = content.slice(startIdx, endIdx).trim().slice(0, 500);
25
+ tasks.push({ id, title, description, phase: 0 });
26
+ }
27
+ return tasks;
28
+ }
29
+ // Push approved plan tasks to GitHub Issues
30
+ export async function syncPlanToGitHub(tasks, phaseNumber, phaseName) {
31
+ const taskToIssue = new Map();
32
+ for (const task of tasks) {
33
+ try {
34
+ const body = `## Phase ${phaseNumber}: ${phaseName}
35
+
36
+ ### Task: ${task.title}
37
+
38
+ ${task.description}
39
+
40
+ ---
41
+ *Created by TDD Dashboard*
42
+ *Task ID: ${task.id}*`;
43
+ const { stdout } = await execAsync(`gh issue create --title "[Phase ${phaseNumber}] ${task.title}" --body "${body.replace(/"/g, '\\"')}" --label "tdd,phase-${phaseNumber}"`, { cwd: process.cwd() });
44
+ // Extract issue number from output
45
+ const issueMatch = stdout.match(/issues\/(\d+)/);
46
+ if (issueMatch) {
47
+ taskToIssue.set(task.id, parseInt(issueMatch[1], 10));
48
+ }
49
+ }
50
+ catch (e) {
51
+ console.error(`Failed to create issue for task ${task.id}:`, e);
52
+ }
53
+ }
54
+ return taskToIssue;
55
+ }
56
+ // Mark issue as in-progress when agent starts
57
+ export async function markIssueInProgress(issueNumber) {
58
+ try {
59
+ await execAsync(`gh issue edit ${issueNumber} --add-label "in-progress" --remove-label "tdd"`, { cwd: process.cwd() });
60
+ }
61
+ catch (e) {
62
+ // Ignore - label might not exist
63
+ }
64
+ }
65
+ // Mark issue as complete when agent finishes
66
+ export async function markIssueComplete(issueNumber) {
67
+ try {
68
+ await execAsync(`gh issue close ${issueNumber} --comment "Completed by TDD agent"`, { cwd: process.cwd() });
69
+ }
70
+ catch (e) {
71
+ // Ignore errors
72
+ }
73
+ }
74
+ // Check if plan is approved (has APPROVED marker or user confirmed)
75
+ export async function isPlanApproved(planPath) {
76
+ if (!existsSync(planPath))
77
+ return false;
78
+ const content = await readFile(planPath, 'utf-8');
79
+ return content.includes('[APPROVED]') || content.includes('Status: Approved');
80
+ }
81
+ // Mark plan as approved
82
+ export async function approvePlan(planPath) {
83
+ if (!existsSync(planPath))
84
+ return;
85
+ const content = await readFile(planPath, 'utf-8');
86
+ const updatedContent = content.replace(/^(#.+)$/m, '$1\n\n> **Status: Approved** - Tasks synced to GitHub');
87
+ const { writeFile } = await import('fs/promises');
88
+ await writeFile(planPath, updatedContent);
89
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,117 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { parseTasksFromPlan, isPlanApproved, approvePlan } from './PlanSync.js';
3
+ import { vol } from 'memfs';
4
+ // Mock fs modules
5
+ vi.mock('fs', async () => {
6
+ const memfs = await import('memfs');
7
+ return memfs.fs;
8
+ });
9
+ vi.mock('fs/promises', async () => {
10
+ const memfs = await import('memfs');
11
+ return memfs.fs.promises;
12
+ });
13
+ describe('PlanSync', () => {
14
+ beforeEach(() => {
15
+ vol.reset();
16
+ });
17
+ afterEach(() => {
18
+ vi.restoreAllMocks();
19
+ });
20
+ describe('parseTasksFromPlan', () => {
21
+ it('returns empty array for non-existent file', async () => {
22
+ const tasks = await parseTasksFromPlan('/fake/path.md');
23
+ expect(tasks).toEqual([]);
24
+ });
25
+ it('parses markdown task format (## Task N: Title)', async () => {
26
+ vol.fromJSON({
27
+ '/plan.md': `# Phase 1 Plan
28
+
29
+ ## Task 1: Setup authentication
30
+ Configure JWT tokens and session management.
31
+
32
+ ## Task 2: Create login endpoint
33
+ Build POST /api/login route.
34
+ `
35
+ });
36
+ const tasks = await parseTasksFromPlan('/plan.md');
37
+ expect(tasks).toHaveLength(2);
38
+ expect(tasks[0]).toMatchObject({
39
+ id: 'task-1',
40
+ title: 'Setup authentication',
41
+ });
42
+ expect(tasks[0].description).toContain('Configure JWT');
43
+ expect(tasks[1]).toMatchObject({
44
+ id: 'task-2',
45
+ title: 'Create login endpoint',
46
+ });
47
+ });
48
+ it('parses XML task format (<task id="...">)', async () => {
49
+ vol.fromJSON({
50
+ '/plan.md': `# Phase 1
51
+
52
+ <task id="01-01">
53
+ Setup the database connection
54
+ </task>
55
+
56
+ <task id="01-02">
57
+ Create user model
58
+ </task>
59
+ `
60
+ });
61
+ const tasks = await parseTasksFromPlan('/plan.md');
62
+ expect(tasks).toHaveLength(2);
63
+ expect(tasks[0].id).toBe('01-01');
64
+ expect(tasks[1].id).toBe('01-02');
65
+ });
66
+ it('truncates description to 500 chars', async () => {
67
+ const longDescription = 'A'.repeat(600);
68
+ vol.fromJSON({
69
+ '/plan.md': `## Task 1: Long task\n${longDescription}`
70
+ });
71
+ const tasks = await parseTasksFromPlan('/plan.md');
72
+ expect(tasks[0].description.length).toBeLessThanOrEqual(500);
73
+ });
74
+ });
75
+ describe('isPlanApproved', () => {
76
+ it('returns false for non-existent file', async () => {
77
+ const result = await isPlanApproved('/fake/plan.md');
78
+ expect(result).toBe(false);
79
+ });
80
+ it('returns true when file contains [APPROVED]', async () => {
81
+ vol.fromJSON({
82
+ '/plan.md': `# Plan [APPROVED]\n\nSome content`
83
+ });
84
+ const result = await isPlanApproved('/plan.md');
85
+ expect(result).toBe(true);
86
+ });
87
+ it('returns true when file contains Status: Approved', async () => {
88
+ vol.fromJSON({
89
+ '/plan.md': `# Plan\n\n> **Status: Approved**\n\nContent`
90
+ });
91
+ const result = await isPlanApproved('/plan.md');
92
+ expect(result).toBe(true);
93
+ });
94
+ it('returns false when not approved', async () => {
95
+ vol.fromJSON({
96
+ '/plan.md': `# Plan\n\nNot yet approved.`
97
+ });
98
+ const result = await isPlanApproved('/plan.md');
99
+ expect(result).toBe(false);
100
+ });
101
+ });
102
+ describe('approvePlan', () => {
103
+ it('does nothing for non-existent file', async () => {
104
+ await approvePlan('/fake/plan.md');
105
+ // No error thrown
106
+ });
107
+ it('adds approval status after first heading', async () => {
108
+ vol.fromJSON({
109
+ '/plan.md': `# My Plan\n\n## Task 1: Something`
110
+ });
111
+ await approvePlan('/plan.md');
112
+ const content = vol.readFileSync('/plan.md', 'utf-8');
113
+ expect(content).toContain('Status: Approved');
114
+ expect(content).toContain('Tasks synced to GitHub');
115
+ });
116
+ });
117
+ });
@@ -0,0 +1,6 @@
1
+ interface PreviewPaneProps {
2
+ isActive: boolean;
3
+ isTTY?: boolean;
4
+ }
5
+ export declare function PreviewPane({ isActive, isTTY }: PreviewPaneProps): import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { useState, useCallback } from 'react';
4
+ import Spinner from 'ink-spinner';
5
+ export function PreviewPane({ isActive, isTTY = true }) {
6
+ const [status, setStatus] = useState('stopped');
7
+ const [url, setUrl] = useState(null);
8
+ const [error, setError] = useState(null);
9
+ const startContainer = useCallback(async () => {
10
+ setStatus('starting');
11
+ setError(null);
12
+ try {
13
+ // TODO: Integrate with dockerode
14
+ // For now, simulate container start
15
+ await new Promise(resolve => setTimeout(resolve, 2000));
16
+ setStatus('running');
17
+ setUrl('http://localhost:3000');
18
+ }
19
+ catch (e) {
20
+ setStatus('error');
21
+ setError(e instanceof Error ? e.message : 'Failed to start container');
22
+ }
23
+ }, []);
24
+ const stopContainer = useCallback(async () => {
25
+ setStatus('stopped');
26
+ setUrl(null);
27
+ }, []);
28
+ useInput((input, key) => {
29
+ if (!isActive)
30
+ return;
31
+ if (input === 's' && status === 'stopped') {
32
+ startContainer();
33
+ }
34
+ else if (input === 'x' && status === 'running') {
35
+ stopContainer();
36
+ }
37
+ else if (input === 'r' && status === 'running') {
38
+ // Restart
39
+ stopContainer().then(startContainer);
40
+ }
41
+ }, { isActive: isTTY });
42
+ return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { children: "Status: " }), status === 'stopped' && _jsx(Text, { color: "gray", children: "Stopped" }), status === 'starting' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { color: "yellow", children: " Starting..." })] })), status === 'running' && _jsx(Text, { color: "green", children: "Running" }), status === 'error' && _jsx(Text, { color: "red", children: "Error" })] }), url && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "cyan", children: url }) })), error && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "red", children: error }) })), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsxs(Text, { dimColor: true, children: [status === 'stopped' && isActive && '[s] Start', status === 'running' && isActive && '[x] Stop | [r] Restart', !isActive && 'Tab to this pane for controls'] }) })] }));
43
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, it, expect } from 'vitest';
3
+ import { render } from 'ink-testing-library';
4
+ import { PreviewPane } from './PreviewPane.js';
5
+ describe('PreviewPane', () => {
6
+ describe('initial state', () => {
7
+ it('shows stopped status initially', () => {
8
+ const { lastFrame } = render(_jsx(PreviewPane, { isActive: false }));
9
+ const output = lastFrame();
10
+ expect(output).toContain('Status:');
11
+ expect(output).toContain('Stopped');
12
+ });
13
+ });
14
+ describe('controls', () => {
15
+ it('shows start control when active and stopped', () => {
16
+ const { lastFrame } = render(_jsx(PreviewPane, { isActive: true }));
17
+ const output = lastFrame();
18
+ expect(output).toContain('[s] Start');
19
+ });
20
+ it('shows hint when inactive', () => {
21
+ const { lastFrame } = render(_jsx(PreviewPane, { isActive: false }));
22
+ const output = lastFrame();
23
+ expect(output).toContain('Tab to this pane');
24
+ });
25
+ });
26
+ describe('render states', () => {
27
+ it('renders without error when active', () => {
28
+ const { lastFrame } = render(_jsx(PreviewPane, { isActive: true }));
29
+ expect(lastFrame()).toBeDefined();
30
+ });
31
+ it('renders without error when inactive', () => {
32
+ const { lastFrame } = render(_jsx(PreviewPane, { isActive: false }));
33
+ expect(lastFrame()).toBeDefined();
34
+ });
35
+ });
36
+ });
@@ -0,0 +1 @@
1
+ export declare function StatusPane(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,32 @@
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { useState, useEffect } from 'react';
4
+ import { exec } from 'child_process';
5
+ import { promisify } from 'util';
6
+ const execAsync = promisify(exec);
7
+ export function StatusPane() {
8
+ const [tests, setTests] = useState(null);
9
+ const [lastRun, setLastRun] = useState('Never');
10
+ useEffect(() => {
11
+ async function checkTests() {
12
+ try {
13
+ // Try to detect test framework and get status
14
+ // This is a simplified version - real implementation would parse test output
15
+ const { stdout } = await execAsync('npm test -- --reporter=json 2>/dev/null || echo "{}"', {
16
+ timeout: 30000,
17
+ cwd: process.cwd()
18
+ });
19
+ // For now, show placeholder
20
+ setTests({ total: 0, passed: 0, failed: 0, pending: 0 });
21
+ setLastRun(new Date().toLocaleTimeString());
22
+ }
23
+ catch (e) {
24
+ // Tests not configured or failed to run
25
+ setTests(null);
26
+ }
27
+ }
28
+ // Check on mount
29
+ checkTests();
30
+ }, []);
31
+ return (_jsx(Box, { padding: 1, flexDirection: "column", children: tests ? (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsxs(Text, { color: "green", children: ["Passed: ", tests.passed] }), _jsx(Text, { children: " | " }), _jsxs(Text, { color: "red", children: ["Failed: ", tests.failed] }), _jsx(Text, { children: " | " }), _jsxs(Text, { color: "yellow", children: ["Pending: ", tests.pending] })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", children: ["Total: ", tests.total] }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Last run: ", lastRun] }) })] })) : (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", children: "No test results." }), _jsx(Text, { dimColor: true, children: "Run tests to see status." })] })) }));
32
+ }
@@ -0,0 +1 @@
1
+ export {};