Skip to content

Commit 4ab968d

Browse files
Fix GitHub Copilot VSCode integration (#7)
* fix: Github Copilot with vscode * Update CHANGELOG
1 parent 43c5710 commit 4ab968d

File tree

6 files changed

+249
-6
lines changed

6 files changed

+249
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
- Fixed Gemini CLI integration https://github.com/codeaholicguy/ai-devkit/issues/3
1111
- Added test for TemplateManager.ts
12+
- Fixed Github Copilot integration https://github.com/codeaholicguy/ai-devkit/issues/4
1213

1314
## [0.4.0] - 2025-10-31
1415

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,8 @@ Supported Tools & Agents:
122122
| Agent | Support | Notes |
123123
|-----------------------------------------------------------|---------|---------------------------------------------------|
124124
| [Claude Code](https://www.anthropic.com/claude-code) || |
125-
| [GitHub Copilot](https://code.visualstudio.com/) | 🚧 | Testing |
126-
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | 🚧 | Testing |
125+
| [GitHub Copilot](https://code.visualstudio.com/) | | VSCode only |
126+
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | | |
127127
| [Cursor](https://cursor.sh/) || |
128128
| [opencode](https://opencode.ai/) | 🚧 | Testing |
129129
| [Windsurf](https://windsurf.com/) | 🚧 | Testing |

src/__tests__/lib/TemplateManager.test.ts

Lines changed: 239 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import * as fs from 'fs-extra';
22
import * as path from 'path';
33
import { TemplateManager } from '../../lib/TemplateManager';
4-
import { EnvironmentDefinition } from '../../types';
4+
import { EnvironmentDefinition, Phase, EnvironmentCode } from '../../types';
55

66
jest.mock('fs-extra');
7+
jest.mock('../../util/env');
78

89
describe('TemplateManager', () => {
910
let templateManager: TemplateManager;
1011
let mockFs: jest.Mocked<typeof fs>;
12+
let mockGetEnvironment: jest.MockedFunction<any>;
1113

1214
beforeEach(() => {
1315
mockFs = fs as jest.Mocked<typeof fs>;
16+
mockGetEnvironment = require('../../util/env').getEnvironment as jest.MockedFunction<any>;
1417
templateManager = new TemplateManager('/test/target');
1518

1619
jest.clearAllMocks();
@@ -218,4 +221,239 @@ describe('TemplateManager', () => {
218221
consoleErrorSpy.mockRestore();
219222
});
220223
});
224+
225+
describe('copyPhaseTemplate', () => {
226+
it('should copy phase template and return target file path', async () => {
227+
const phase: Phase = 'requirements';
228+
229+
(mockFs.ensureDir as any).mockResolvedValue(undefined);
230+
(mockFs.copy as any).mockResolvedValue(undefined);
231+
232+
const result = await templateManager.copyPhaseTemplate(phase);
233+
234+
expect(mockFs.ensureDir).toHaveBeenCalledWith(
235+
path.join(templateManager['targetDir'], 'docs', 'ai', phase)
236+
);
237+
expect(mockFs.copy).toHaveBeenCalledWith(
238+
path.join(templateManager['templatesDir'], 'phases', `${phase}.md`),
239+
path.join(templateManager['targetDir'], 'docs', 'ai', phase, 'README.md')
240+
);
241+
expect(result).toBe(path.join(templateManager['targetDir'], 'docs', 'ai', phase, 'README.md'));
242+
});
243+
});
244+
245+
describe('fileExists', () => {
246+
it('should return true when phase file exists', async () => {
247+
const phase: Phase = 'design';
248+
249+
(mockFs.pathExists as any).mockResolvedValue(true);
250+
251+
const result = await templateManager.fileExists(phase);
252+
253+
expect(mockFs.pathExists).toHaveBeenCalledWith(
254+
path.join(templateManager['targetDir'], 'docs', 'ai', phase, 'README.md')
255+
);
256+
expect(result).toBe(true);
257+
});
258+
259+
it('should return false when phase file does not exist', async () => {
260+
const phase: Phase = 'planning';
261+
262+
(mockFs.pathExists as any).mockResolvedValue(false);
263+
264+
const result = await templateManager.fileExists(phase);
265+
266+
expect(mockFs.pathExists).toHaveBeenCalledWith(
267+
path.join(templateManager['targetDir'], 'docs', 'ai', phase, 'README.md')
268+
);
269+
expect(result).toBe(false);
270+
});
271+
});
272+
273+
describe('setupMultipleEnvironments', () => {
274+
it('should setup multiple environments successfully', async () => {
275+
const envIds: EnvironmentCode[] = ['cursor', 'gemini'];
276+
const cursorEnv = {
277+
code: 'cursor',
278+
name: 'Cursor',
279+
contextFileName: 'AGENTS.md',
280+
commandPath: '.cursor/commands',
281+
};
282+
const geminiEnv = {
283+
code: 'gemini',
284+
name: 'Gemini',
285+
contextFileName: 'AGENTS.md',
286+
commandPath: '.gemini/commands',
287+
isCustomCommandPath: true,
288+
};
289+
290+
mockGetEnvironment
291+
.mockReturnValueOnce(cursorEnv)
292+
.mockReturnValueOnce(geminiEnv);
293+
294+
// Mock setupSingleEnvironment
295+
const mockSetupSingleEnvironment = jest.fn();
296+
mockSetupSingleEnvironment
297+
.mockResolvedValueOnce(['/path/to/cursor/file1', '/path/to/cursor/file2'])
298+
.mockResolvedValueOnce(['/path/to/gemini/file1']);
299+
300+
(templateManager as any).setupSingleEnvironment = mockSetupSingleEnvironment;
301+
302+
const result = await templateManager.setupMultipleEnvironments(envIds);
303+
304+
expect(mockGetEnvironment).toHaveBeenCalledWith('cursor');
305+
expect(mockGetEnvironment).toHaveBeenCalledWith('gemini');
306+
expect(mockSetupSingleEnvironment).toHaveBeenCalledWith(cursorEnv);
307+
expect(mockSetupSingleEnvironment).toHaveBeenCalledWith(geminiEnv);
308+
expect(result).toEqual([
309+
'/path/to/cursor/file1',
310+
'/path/to/cursor/file2',
311+
'/path/to/gemini/file1'
312+
]);
313+
});
314+
315+
it('should skip invalid environments and continue with valid ones', async () => {
316+
const envIds: EnvironmentCode[] = ['cursor', 'invalid' as any, 'gemini'];
317+
const cursorEnv = {
318+
code: 'cursor',
319+
name: 'Cursor',
320+
contextFileName: 'AGENTS.md',
321+
commandPath: '.cursor/commands',
322+
};
323+
const geminiEnv = {
324+
code: 'gemini',
325+
name: 'Gemini',
326+
contextFileName: 'AGENTS.md',
327+
commandPath: '.gemini/commands',
328+
isCustomCommandPath: true,
329+
};
330+
331+
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
332+
333+
mockGetEnvironment
334+
.mockReturnValueOnce(cursorEnv)
335+
.mockReturnValueOnce(undefined) // invalid environment
336+
.mockReturnValueOnce(geminiEnv);
337+
338+
// Mock setupSingleEnvironment
339+
const mockSetupSingleEnvironment = jest.fn();
340+
mockSetupSingleEnvironment
341+
.mockResolvedValueOnce(['/path/to/cursor/file1'])
342+
.mockResolvedValueOnce(['/path/to/gemini/file1']);
343+
344+
(templateManager as any).setupSingleEnvironment = mockSetupSingleEnvironment;
345+
346+
const result = await templateManager.setupMultipleEnvironments(envIds);
347+
348+
expect(consoleWarnSpy).toHaveBeenCalledWith("Warning: Environment 'invalid' not found, skipping");
349+
expect(result).toEqual([
350+
'/path/to/cursor/file1',
351+
'/path/to/gemini/file1'
352+
]);
353+
354+
consoleWarnSpy.mockRestore();
355+
});
356+
357+
it('should throw error when setupSingleEnvironment fails', async () => {
358+
const envIds: EnvironmentCode[] = ['cursor'];
359+
const cursorEnv = {
360+
code: 'cursor',
361+
name: 'Cursor',
362+
contextFileName: 'AGENTS.md',
363+
commandPath: '.cursor/commands',
364+
};
365+
366+
mockGetEnvironment.mockReturnValue(cursorEnv);
367+
368+
const mockSetupSingleEnvironment = jest.fn().mockRejectedValue(new Error('Setup failed'));
369+
(templateManager as any).setupSingleEnvironment = mockSetupSingleEnvironment;
370+
371+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
372+
373+
await expect(templateManager.setupMultipleEnvironments(envIds)).rejects.toThrow('Setup failed');
374+
375+
expect(consoleErrorSpy).toHaveBeenCalledWith("Error setting up environment 'Cursor':", expect.any(Error));
376+
377+
consoleErrorSpy.mockRestore();
378+
});
379+
});
380+
381+
describe('checkEnvironmentExists', () => {
382+
it('should return false when environment does not exist', async () => {
383+
const envId: EnvironmentCode = 'cursor';
384+
385+
mockGetEnvironment.mockReturnValue(undefined);
386+
387+
const result = await templateManager.checkEnvironmentExists(envId);
388+
389+
expect(mockGetEnvironment).toHaveBeenCalledWith(envId);
390+
expect(result).toBe(false);
391+
});
392+
393+
it('should return true when context file exists', async () => {
394+
const envId: EnvironmentCode = 'cursor';
395+
const env = {
396+
code: 'cursor',
397+
name: 'Cursor',
398+
contextFileName: 'AGENTS.md',
399+
commandPath: '.cursor/commands',
400+
};
401+
402+
mockGetEnvironment.mockReturnValue(env);
403+
404+
(mockFs.pathExists as any)
405+
.mockResolvedValueOnce(true) // context file exists
406+
.mockResolvedValueOnce(false); // command dir doesn't exist
407+
408+
const result = await templateManager.checkEnvironmentExists(envId);
409+
410+
expect(mockFs.pathExists).toHaveBeenCalledWith(
411+
path.join(templateManager['targetDir'], env.contextFileName)
412+
);
413+
expect(mockFs.pathExists).toHaveBeenCalledWith(
414+
path.join(templateManager['targetDir'], env.commandPath)
415+
);
416+
expect(result).toBe(true);
417+
});
418+
419+
it('should return true when command directory exists', async () => {
420+
const envId: EnvironmentCode = 'cursor';
421+
const env = {
422+
code: 'cursor',
423+
name: 'Cursor',
424+
contextFileName: 'AGENTS.md',
425+
commandPath: '.cursor/commands',
426+
};
427+
428+
mockGetEnvironment.mockReturnValue(env);
429+
430+
(mockFs.pathExists as any)
431+
.mockResolvedValueOnce(false) // context file doesn't exist
432+
.mockResolvedValueOnce(true); // command dir exists
433+
434+
const result = await templateManager.checkEnvironmentExists(envId);
435+
436+
expect(result).toBe(true);
437+
});
438+
439+
it('should return false when neither context file nor command directory exists', async () => {
440+
const envId: EnvironmentCode = 'cursor';
441+
const env = {
442+
code: 'cursor',
443+
name: 'Cursor',
444+
contextFileName: 'AGENTS.md',
445+
commandPath: '.cursor/commands',
446+
};
447+
448+
mockGetEnvironment.mockReturnValue(env);
449+
450+
(mockFs.pathExists as any)
451+
.mockResolvedValueOnce(false) // context file doesn't exist
452+
.mockResolvedValueOnce(false); // command dir doesn't exist
453+
454+
const result = await templateManager.checkEnvironmentExists(envId);
455+
456+
expect(result).toBe(false);
457+
});
458+
});
221459
});

src/lib/TemplateManager.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export class TemplateManager {
117117
copiedFiles: string[]
118118
): Promise<void> {
119119
const commandsSourceDir = path.join(this.templatesDir, "commands");
120+
const commandExtension = env.customCommandExtension || ".md";
120121
const commandsTargetDir = path.join(this.targetDir, env.commandPath);
121122

122123
if (await fs.pathExists(commandsSourceDir)) {
@@ -127,11 +128,12 @@ export class TemplateManager {
127128
commandFiles
128129
.filter((file: string) => file.endsWith(".md"))
129130
.map(async (file: string) => {
131+
const targetFile = file.replace('.md', commandExtension);
130132
await fs.copy(
131133
path.join(commandsSourceDir, file),
132-
path.join(commandsTargetDir, file)
134+
path.join(commandsTargetDir, targetFile)
133135
);
134-
copiedFiles.push(path.join(commandsTargetDir, file));
136+
copiedFiles.push(path.join(commandsTargetDir, targetFile));
135137
})
136138
);
137139
} else {

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface EnvironmentDefinition {
1414
commandPath: string;
1515
description?: string;
1616
isCustomCommandPath?: boolean;
17+
customCommandExtension?: string;
1718
}
1819

1920
export type EnvironmentCode = 'cursor' | 'claude' | 'github' | 'gemini' | 'codex' | 'windsurf' | 'kilocode' | 'amp' | 'opencode' | 'roo';

src/util/env.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ export const ENVIRONMENT_DEFINITIONS: Record<EnvironmentCode, EnvironmentDefinit
1717
code: 'github',
1818
name: 'GitHub Copilot',
1919
contextFileName: 'AGENTS.md',
20-
commandPath: '.github/commands',
20+
commandPath: '.github/prompts',
21+
customCommandExtension: '.prompt.md',
2122
},
2223
gemini: {
2324
code: 'gemini',

0 commit comments

Comments
 (0)