Skip to content

Commit 43c5710

Browse files
Fix Gemini template integration and enhance status indicators (#6)
* fix: enhance TemplateManager for Gemini integration and improve status indicators * Update command templates * Update result display * Update standard copy logic * Update CHANGELOG
1 parent 5934368 commit 43c5710

19 files changed

+863
-41
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## Unreleased
9+
10+
- Fixed Gemini CLI integration https://github.com/codeaholicguy/ai-devkit/issues/3
11+
- Added test for TemplateManager.ts
12+
813
## [0.4.0] - 2025-10-31
914

1015
### Added

src/__tests__/lib/EnvironmentSelector.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,16 +135,16 @@ describe('EnvironmentSelector', () => {
135135
selector.displaySelectionSummary(['cursor', 'claude']);
136136

137137
expect(consoleSpy).toHaveBeenCalledWith('\nSelected environments:');
138-
expect(consoleSpy).toHaveBeenCalledWith(' [OK] Cursor');
139-
expect(consoleSpy).toHaveBeenCalledWith(' [OK] Claude Code');
138+
expect(consoleSpy).toHaveBeenCalledWith(' Cursor');
139+
expect(consoleSpy).toHaveBeenCalledWith(' Claude Code');
140140
expect(consoleSpy).toHaveBeenCalledWith('');
141141
});
142142

143143
it('should handle single environment selection', () => {
144144
selector.displaySelectionSummary(['cursor']);
145145

146146
expect(consoleSpy).toHaveBeenCalledWith('\nSelected environments:');
147-
expect(consoleSpy).toHaveBeenCalledWith(' [OK] Cursor');
147+
expect(consoleSpy).toHaveBeenCalledWith(' Cursor');
148148
expect(consoleSpy).toHaveBeenCalledWith('');
149149
});
150150
});

src/__tests__/lib/PhaseSelector.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,16 +82,16 @@ describe('PhaseSelector', () => {
8282
selector.displaySelectionSummary(['requirements', 'design']);
8383

8484
expect(consoleSpy).toHaveBeenCalledWith('\nSelected phases:');
85-
expect(consoleSpy).toHaveBeenCalledWith(' [OK] Requirements & Problem Understanding');
86-
expect(consoleSpy).toHaveBeenCalledWith(' [OK] System Design & Architecture');
85+
expect(consoleSpy).toHaveBeenCalledWith(' Requirements & Problem Understanding');
86+
expect(consoleSpy).toHaveBeenCalledWith(' System Design & Architecture');
8787
expect(consoleSpy).toHaveBeenCalledWith('');
8888
});
8989

9090
it('should handle single phase selection', () => {
9191
selector.displaySelectionSummary(['requirements']);
9292

9393
expect(consoleSpy).toHaveBeenCalledWith('\nSelected phases:');
94-
expect(consoleSpy).toHaveBeenCalledWith(' [OK] Requirements & Problem Understanding');
94+
expect(consoleSpy).toHaveBeenCalledWith(' Requirements & Problem Understanding');
9595
expect(consoleSpy).toHaveBeenCalledWith('');
9696
});
9797
});
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import * as fs from 'fs-extra';
2+
import * as path from 'path';
3+
import { TemplateManager } from '../../lib/TemplateManager';
4+
import { EnvironmentDefinition } from '../../types';
5+
6+
jest.mock('fs-extra');
7+
8+
describe('TemplateManager', () => {
9+
let templateManager: TemplateManager;
10+
let mockFs: jest.Mocked<typeof fs>;
11+
12+
beforeEach(() => {
13+
mockFs = fs as jest.Mocked<typeof fs>;
14+
templateManager = new TemplateManager('/test/target');
15+
16+
jest.clearAllMocks();
17+
});
18+
19+
afterEach(() => {
20+
jest.restoreAllMocks();
21+
});
22+
23+
describe('setupSingleEnvironment', () => {
24+
it('should copy context file when it exists', async () => {
25+
const env: EnvironmentDefinition = {
26+
code: 'test-env',
27+
name: 'Test Environment',
28+
contextFileName: '.test-context.md',
29+
commandPath: '.test',
30+
isCustomCommandPath: false
31+
};
32+
33+
(mockFs.pathExists as any)
34+
.mockResolvedValueOnce(true)
35+
.mockResolvedValueOnce(true);
36+
37+
(mockFs.readdir as any).mockResolvedValue(['command1.md', 'command2.toml']);
38+
39+
const result = await (templateManager as any).setupSingleEnvironment(env);
40+
41+
expect(mockFs.copy).toHaveBeenCalledWith(
42+
path.join(templateManager['templatesDir'], 'env', 'base.md'),
43+
path.join(templateManager['targetDir'], env.contextFileName)
44+
);
45+
expect(result).toContain(path.join(templateManager['targetDir'], env.contextFileName));
46+
});
47+
48+
it('should warn when context file does not exist', async () => {
49+
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
50+
51+
const env: EnvironmentDefinition = {
52+
code: 'test-env',
53+
name: 'Test Environment',
54+
contextFileName: '.test-context.md',
55+
commandPath: '.test',
56+
isCustomCommandPath: false
57+
};
58+
59+
(mockFs.pathExists as any)
60+
.mockResolvedValueOnce(false)
61+
.mockResolvedValueOnce(true);
62+
63+
(mockFs.readdir as any).mockResolvedValue(['command1.md']);
64+
65+
const result = await (templateManager as any).setupSingleEnvironment(env);
66+
67+
expect(consoleWarnSpy).toHaveBeenCalledWith(
68+
expect.stringContaining('Warning: Context file not found')
69+
);
70+
expect(result).toEqual([path.join(templateManager['targetDir'], env.commandPath, 'command1.md')]);
71+
72+
consoleWarnSpy.mockRestore();
73+
});
74+
75+
it('should copy commands when isCustomCommandPath is false', async () => {
76+
const env: EnvironmentDefinition = {
77+
code: 'test-env',
78+
name: 'Test Environment',
79+
contextFileName: '.test-context.md',
80+
commandPath: '.test',
81+
isCustomCommandPath: false
82+
};
83+
84+
const mockCommandFiles = ['command1.md', 'command2.toml', 'command3.md'];
85+
86+
(mockFs.pathExists as any)
87+
.mockResolvedValueOnce(true) // context file exists
88+
.mockResolvedValueOnce(true); // commands directory exists
89+
90+
(mockFs.readdir as any).mockResolvedValue(mockCommandFiles);
91+
92+
const result = await (templateManager as any).setupSingleEnvironment(env);
93+
94+
expect(mockFs.ensureDir).toHaveBeenCalledWith(
95+
path.join(templateManager['targetDir'], env.commandPath)
96+
);
97+
98+
// Should only copy .md files (not .toml files)
99+
expect(mockFs.copy).toHaveBeenCalledWith(
100+
path.join(templateManager['templatesDir'], 'commands', 'command1.md'),
101+
path.join(templateManager['targetDir'], env.commandPath, 'command1.md')
102+
);
103+
expect(mockFs.copy).toHaveBeenCalledWith(
104+
path.join(templateManager['templatesDir'], 'commands', 'command3.md'),
105+
path.join(templateManager['targetDir'], env.commandPath, 'command3.md')
106+
);
107+
108+
expect(result).toContain(path.join(templateManager['targetDir'], env.commandPath, 'command1.md'));
109+
expect(result).toContain(path.join(templateManager['targetDir'], env.commandPath, 'command3.md'));
110+
});
111+
112+
it('should skip commands when isCustomCommandPath is true', async () => {
113+
const env: EnvironmentDefinition = {
114+
code: 'test-env',
115+
name: 'Test Environment',
116+
contextFileName: '.test-context.md',
117+
commandPath: '.test',
118+
isCustomCommandPath: true
119+
};
120+
121+
(mockFs.pathExists as any).mockResolvedValueOnce(true);
122+
123+
const result = await (templateManager as any).setupSingleEnvironment(env);
124+
125+
expect(mockFs.ensureDir).not.toHaveBeenCalled();
126+
expect(mockFs.copy).toHaveBeenCalledTimes(1);
127+
expect(result).toContain(path.join(templateManager['targetDir'], env.contextFileName));
128+
});
129+
130+
it('should handle cursor environment with special files', async () => {
131+
const env: EnvironmentDefinition = {
132+
code: 'cursor',
133+
name: 'Cursor',
134+
contextFileName: '.cursor.md',
135+
commandPath: '.cursor',
136+
isCustomCommandPath: false
137+
};
138+
139+
const mockRuleFiles = ['rule1.md', 'rule2.toml'];
140+
141+
(mockFs.pathExists as any)
142+
.mockResolvedValueOnce(true)
143+
.mockResolvedValueOnce(true) .mockResolvedValueOnce(true);
144+
145+
(mockFs.readdir as any)
146+
.mockResolvedValueOnce([]) .mockResolvedValueOnce(mockRuleFiles);
147+
const result = await (templateManager as any).setupSingleEnvironment(env);
148+
149+
expect(mockFs.ensureDir).toHaveBeenCalledWith(
150+
path.join(templateManager['targetDir'], '.cursor', 'rules')
151+
);
152+
expect(mockFs.copy).toHaveBeenCalledWith(
153+
path.join(templateManager['templatesDir'], 'env', 'cursor', 'rules'),
154+
path.join(templateManager['targetDir'], '.cursor', 'rules')
155+
);
156+
157+
expect(result).toContain(path.join(templateManager['targetDir'], '.cursor', 'rules', 'rule1.md'));
158+
expect(result).toContain(path.join(templateManager['targetDir'], '.cursor', 'rules', 'rule2.toml'));
159+
});
160+
161+
it('should handle gemini environment with toml files', async () => {
162+
const env: EnvironmentDefinition = {
163+
code: 'gemini',
164+
name: 'Gemini',
165+
contextFileName: '.gemini.md',
166+
commandPath: '.gemini',
167+
isCustomCommandPath: false
168+
};
169+
170+
const mockCommandFiles = ['command1.md', 'command2.toml', 'command3.toml'];
171+
172+
(mockFs.pathExists as any)
173+
.mockResolvedValueOnce(true)
174+
.mockResolvedValueOnce(true) .mockResolvedValueOnce(true); // gemini commands directory exists
175+
176+
(mockFs.readdir as any).mockResolvedValue(mockCommandFiles);
177+
178+
const result = await (templateManager as any).setupSingleEnvironment(env);
179+
180+
expect(mockFs.ensureDir).toHaveBeenCalledWith(
181+
path.join(templateManager['targetDir'], '.gemini', 'commands')
182+
);
183+
184+
expect(mockFs.copy).toHaveBeenCalledWith(
185+
path.join(templateManager['templatesDir'], 'commands', 'command2.toml'),
186+
path.join(templateManager['targetDir'], '.gemini', 'commands', 'command2.toml')
187+
);
188+
expect(mockFs.copy).toHaveBeenCalledWith(
189+
path.join(templateManager['templatesDir'], 'commands', 'command3.toml'),
190+
path.join(templateManager['targetDir'], '.gemini', 'commands', 'command3.toml')
191+
);
192+
193+
expect(result).toContain(path.join(templateManager['targetDir'], '.gemini', 'commands', 'command2.toml'));
194+
expect(result).toContain(path.join(templateManager['targetDir'], '.gemini', 'commands', 'command3.toml'));
195+
});
196+
197+
it('should handle errors and rethrow them', async () => {
198+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
199+
200+
const env: EnvironmentDefinition = {
201+
code: 'test-env',
202+
name: 'Test Environment',
203+
contextFileName: '.test-context.md',
204+
commandPath: '.test',
205+
isCustomCommandPath: false
206+
};
207+
208+
const testError = new Error('Test error');
209+
(mockFs.pathExists as any).mockRejectedValue(testError);
210+
211+
await expect((templateManager as any).setupSingleEnvironment(env)).rejects.toThrow('Test error');
212+
213+
expect(consoleErrorSpy).toHaveBeenCalledWith(
214+
'Error setting up environment Test Environment:',
215+
testError
216+
);
217+
218+
consoleErrorSpy.mockRestore();
219+
});
220+
});
221+
});

src/lib/EnvironmentSelector.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export class EnvironmentSelector {
5858

5959
console.log('\nSelected environments:');
6060
selected.forEach(envId => {
61-
console.log(` [OK] ${getEnvironmentDisplayName(envId)}`);
61+
console.log(` ${getEnvironmentDisplayName(envId)}`);
6262
});
6363
console.log('');
6464
}

src/lib/PhaseSelector.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export class PhaseSelector {
4949

5050
console.log('\nSelected phases:');
5151
selected.forEach(phase => {
52-
console.log(` [OK] ${PHASE_DISPLAY_NAMES[phase]}`);
52+
console.log(` ${PHASE_DISPLAY_NAMES[phase]}`);
5353
});
5454
console.log('');
5555
}

0 commit comments

Comments
 (0)