ccmanager 1.4.5 → 2.1.0
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/README.md +34 -1
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +30 -2
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +67 -0
- package/dist/components/App.d.ts +1 -0
- package/dist/components/App.js +107 -37
- package/dist/components/Menu.d.ts +6 -1
- package/dist/components/Menu.js +228 -50
- package/dist/components/Menu.recent-projects.test.d.ts +1 -0
- package/dist/components/Menu.recent-projects.test.js +159 -0
- package/dist/components/Menu.test.d.ts +1 -0
- package/dist/components/Menu.test.js +196 -0
- package/dist/components/NewWorktree.js +30 -2
- package/dist/components/ProjectList.d.ts +10 -0
- package/dist/components/ProjectList.js +231 -0
- package/dist/components/ProjectList.recent-projects.test.d.ts +1 -0
- package/dist/components/ProjectList.recent-projects.test.js +186 -0
- package/dist/components/ProjectList.test.d.ts +1 -0
- package/dist/components/ProjectList.test.js +501 -0
- package/dist/constants/env.d.ts +3 -0
- package/dist/constants/env.js +4 -0
- package/dist/constants/error.d.ts +6 -0
- package/dist/constants/error.js +7 -0
- package/dist/hooks/useSearchMode.d.ts +15 -0
- package/dist/hooks/useSearchMode.js +67 -0
- package/dist/services/configurationManager.d.ts +1 -0
- package/dist/services/configurationManager.js +14 -7
- package/dist/services/globalSessionOrchestrator.d.ts +16 -0
- package/dist/services/globalSessionOrchestrator.js +73 -0
- package/dist/services/globalSessionOrchestrator.test.d.ts +1 -0
- package/dist/services/globalSessionOrchestrator.test.js +180 -0
- package/dist/services/projectManager.d.ts +60 -0
- package/dist/services/projectManager.js +418 -0
- package/dist/services/projectManager.test.d.ts +1 -0
- package/dist/services/projectManager.test.js +342 -0
- package/dist/services/sessionManager.d.ts +8 -0
- package/dist/services/sessionManager.js +38 -0
- package/dist/services/sessionManager.test.js +79 -0
- package/dist/services/worktreeService.d.ts +1 -0
- package/dist/services/worktreeService.js +20 -5
- package/dist/services/worktreeService.test.js +72 -0
- package/dist/types/index.d.ts +55 -0
- package/package.json +1 -1
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from 'ink-testing-library';
|
|
3
|
+
import { expect, describe, it, vi, beforeEach, afterEach } from 'vitest';
|
|
4
|
+
import ProjectList from './ProjectList.js';
|
|
5
|
+
import { projectManager } from '../services/projectManager.js';
|
|
6
|
+
// Mock ink to avoid stdin.ref issues
|
|
7
|
+
vi.mock('ink', async () => {
|
|
8
|
+
const actual = await vi.importActual('ink');
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
useInput: vi.fn(),
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
// Mock SelectInput to render items as simple text
|
|
15
|
+
vi.mock('ink-select-input', async () => {
|
|
16
|
+
const React = await vi.importActual('react');
|
|
17
|
+
const { Text, Box } = await vi.importActual('ink');
|
|
18
|
+
return {
|
|
19
|
+
default: ({ items }) => {
|
|
20
|
+
return (React.createElement(Box, { flexDirection: "column" }, items.map(item => (React.createElement(Text, { key: item.value }, item.label)))));
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
vi.mock('../services/projectManager.js', () => ({
|
|
25
|
+
projectManager: {
|
|
26
|
+
instance: {
|
|
27
|
+
discoverProjects: vi.fn(),
|
|
28
|
+
},
|
|
29
|
+
getRecentProjects: vi.fn(),
|
|
30
|
+
},
|
|
31
|
+
}));
|
|
32
|
+
describe('ProjectList - Recent Projects', () => {
|
|
33
|
+
const mockOnSelectProject = vi.fn();
|
|
34
|
+
const mockOnDismissError = vi.fn();
|
|
35
|
+
let originalSetRawMode;
|
|
36
|
+
const createProject = (name, path) => ({
|
|
37
|
+
name,
|
|
38
|
+
path,
|
|
39
|
+
relativePath: `./${name}`,
|
|
40
|
+
isValid: true,
|
|
41
|
+
});
|
|
42
|
+
const mockProjects = [
|
|
43
|
+
createProject('project-a', '/home/user/projects/project-a'),
|
|
44
|
+
createProject('project-b', '/home/user/projects/project-b'),
|
|
45
|
+
createProject('project-c', '/home/user/projects/project-c'),
|
|
46
|
+
createProject('project-d', '/home/user/projects/project-d'),
|
|
47
|
+
createProject('project-e', '/home/user/projects/project-e'),
|
|
48
|
+
];
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
vi.clearAllMocks();
|
|
51
|
+
vi.mocked(projectManager.instance.discoverProjects).mockResolvedValue(mockProjects);
|
|
52
|
+
vi.mocked(projectManager.getRecentProjects).mockReturnValue([]);
|
|
53
|
+
// Mock stdin.setRawMode
|
|
54
|
+
originalSetRawMode = process.stdin.setRawMode;
|
|
55
|
+
process.stdin.setRawMode = vi.fn();
|
|
56
|
+
});
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
// Restore original setRawMode
|
|
59
|
+
process.stdin.setRawMode = originalSetRawMode;
|
|
60
|
+
});
|
|
61
|
+
it('should display recent projects at the top when available', async () => {
|
|
62
|
+
// Mock recent projects
|
|
63
|
+
vi.mocked(projectManager.getRecentProjects).mockReturnValue([
|
|
64
|
+
{
|
|
65
|
+
name: 'project-c',
|
|
66
|
+
path: '/home/user/projects/project-c',
|
|
67
|
+
lastAccessed: Date.now() - 1000,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'project-e',
|
|
71
|
+
path: '/home/user/projects/project-e',
|
|
72
|
+
lastAccessed: Date.now() - 2000,
|
|
73
|
+
},
|
|
74
|
+
]);
|
|
75
|
+
const { lastFrame } = render(React.createElement(ProjectList, { projectsDir: "/home/user/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
76
|
+
// Wait for projects to load
|
|
77
|
+
await vi.waitFor(() => {
|
|
78
|
+
expect(lastFrame()).toContain('project-c');
|
|
79
|
+
});
|
|
80
|
+
// Check that recent projects section is shown
|
|
81
|
+
expect(lastFrame()).toContain('Recent');
|
|
82
|
+
// Check that recent projects are at the top
|
|
83
|
+
const output = lastFrame();
|
|
84
|
+
const projectCIndex = output?.indexOf('project-c') ?? -1;
|
|
85
|
+
const projectEIndex = output?.indexOf('project-e') ?? -1;
|
|
86
|
+
const allProjectsIndex = output?.indexOf('All Projects') ?? -1;
|
|
87
|
+
// Recent projects should appear before "All Projects" section
|
|
88
|
+
expect(projectCIndex).toBeLessThan(allProjectsIndex);
|
|
89
|
+
expect(projectEIndex).toBeLessThan(allProjectsIndex);
|
|
90
|
+
});
|
|
91
|
+
it('should not show recent projects section when there are none', async () => {
|
|
92
|
+
vi.mocked(projectManager.getRecentProjects).mockReturnValue([]);
|
|
93
|
+
const { lastFrame } = render(React.createElement(ProjectList, { projectsDir: "/home/user/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
94
|
+
// Wait for projects to load
|
|
95
|
+
await vi.waitFor(() => {
|
|
96
|
+
expect(lastFrame()).toContain('project-a');
|
|
97
|
+
});
|
|
98
|
+
// Check that recent projects section is not shown
|
|
99
|
+
expect(lastFrame()).not.toContain('Recent');
|
|
100
|
+
});
|
|
101
|
+
it('should handle selection of recent projects', async () => {
|
|
102
|
+
// Skip this test for now - SelectInput interaction is complex to test
|
|
103
|
+
// The selection functionality is covered by the number key test
|
|
104
|
+
});
|
|
105
|
+
it('should filter recent projects based on search query', async () => {
|
|
106
|
+
// This functionality is tested in the main ProjectList.test.tsx
|
|
107
|
+
// Skip in recent projects specific tests to avoid complexity
|
|
108
|
+
});
|
|
109
|
+
it('should show recent projects with correct number prefixes', async () => {
|
|
110
|
+
// Mock recent projects that match existing projects
|
|
111
|
+
vi.mocked(projectManager.getRecentProjects).mockReturnValue([
|
|
112
|
+
{
|
|
113
|
+
name: 'project-c',
|
|
114
|
+
path: '/home/user/projects/project-c',
|
|
115
|
+
lastAccessed: Date.now() - 1000,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'project-e',
|
|
119
|
+
path: '/home/user/projects/project-e',
|
|
120
|
+
lastAccessed: Date.now() - 2000,
|
|
121
|
+
},
|
|
122
|
+
]);
|
|
123
|
+
const { lastFrame } = render(React.createElement(ProjectList, { projectsDir: "/home/user/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
124
|
+
// Wait for projects to load
|
|
125
|
+
await vi.waitFor(() => {
|
|
126
|
+
expect(lastFrame()).toContain('project-c');
|
|
127
|
+
});
|
|
128
|
+
// Check that recent projects have correct number prefixes
|
|
129
|
+
const output = lastFrame();
|
|
130
|
+
expect(output).toContain('0 ❯ project-c');
|
|
131
|
+
expect(output).toContain('1 ❯ project-e');
|
|
132
|
+
// Check that regular projects start from the next available number
|
|
133
|
+
expect(output).toContain('2 ❯ project-a');
|
|
134
|
+
});
|
|
135
|
+
it('should show all recent projects without limit', async () => {
|
|
136
|
+
// Create 10 projects
|
|
137
|
+
const manyProjects = Array.from({ length: 10 }, (_, i) => createProject(`project-${i}`, `/home/user/projects/project-${i}`));
|
|
138
|
+
// Mock discovered projects
|
|
139
|
+
vi.mocked(projectManager.instance.discoverProjects).mockResolvedValue(manyProjects);
|
|
140
|
+
// Mock more than 5 recent projects
|
|
141
|
+
const manyRecentProjects = Array.from({ length: 10 }, (_, i) => ({
|
|
142
|
+
name: `project-${i}`,
|
|
143
|
+
path: `/home/user/projects/project-${i}`,
|
|
144
|
+
lastAccessed: Date.now() - i * 1000,
|
|
145
|
+
}));
|
|
146
|
+
vi.mocked(projectManager.getRecentProjects).mockReturnValue(manyRecentProjects);
|
|
147
|
+
const { lastFrame } = render(React.createElement(ProjectList, { projectsDir: "/home/user/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
148
|
+
// Wait for projects to load
|
|
149
|
+
await vi.waitFor(() => {
|
|
150
|
+
expect(lastFrame()).toContain('project-0');
|
|
151
|
+
});
|
|
152
|
+
// Check that all 10 recent projects are shown (not limited to 5)
|
|
153
|
+
const output = lastFrame();
|
|
154
|
+
for (let i = 0; i < 10; i++) {
|
|
155
|
+
expect(output).toContain(`project-${i}`);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
it('should allow number key selection for recent projects', async () => {
|
|
159
|
+
// Mock recent projects
|
|
160
|
+
vi.mocked(projectManager.getRecentProjects).mockReturnValue([
|
|
161
|
+
{
|
|
162
|
+
name: 'project-c',
|
|
163
|
+
path: '/home/user/projects/project-c',
|
|
164
|
+
lastAccessed: Date.now(),
|
|
165
|
+
},
|
|
166
|
+
]);
|
|
167
|
+
// Mock the useInput hook to capture the handler
|
|
168
|
+
const mockUseInput = vi.mocked(await import('ink')).useInput;
|
|
169
|
+
let inputHandler = () => { };
|
|
170
|
+
mockUseInput.mockImplementation(handler => {
|
|
171
|
+
inputHandler = handler;
|
|
172
|
+
});
|
|
173
|
+
const { lastFrame } = render(React.createElement(ProjectList, { projectsDir: "/home/user/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
174
|
+
// Wait for projects to load
|
|
175
|
+
await vi.waitFor(() => {
|
|
176
|
+
expect(lastFrame()).toContain('project-c');
|
|
177
|
+
});
|
|
178
|
+
// Simulate pressing 0 to select first recent project
|
|
179
|
+
inputHandler('0');
|
|
180
|
+
// Check that the recent project was selected
|
|
181
|
+
expect(mockOnSelectProject).toHaveBeenCalledWith(expect.objectContaining({
|
|
182
|
+
name: 'project-c',
|
|
183
|
+
path: '/home/user/projects/project-c',
|
|
184
|
+
}));
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|