mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-13 23:14:20 +00:00
feat: add CodeBuddy and Qoder IDE support
This commit is contained in:
parent
ea418c0126
commit
419daf776b
4 changed files with 324 additions and 16 deletions
|
|
@ -2,7 +2,7 @@
|
|||
* AI Context Generator
|
||||
*
|
||||
* Creates AGENTS.md and CLAUDE.md with full inline GitNexus context.
|
||||
* AGENTS.md is the standard read by Cursor, Windsurf, OpenCode, Codex, Cline, etc.
|
||||
* AGENTS.md is the standard read by Cursor, Windsurf, OpenCode, Codex, Cline, CodeBuddy, Qoder, etc.
|
||||
* CLAUDE.md is for Claude Code which only reads that file.
|
||||
*/
|
||||
|
||||
|
|
@ -300,11 +300,5 @@ export async function generateAIContextFiles(
|
|||
createdFiles.push('CLAUDE.md (skipped via --skip-agents-md)');
|
||||
}
|
||||
|
||||
// Install skills to .claude/skills/gitnexus/
|
||||
const installedSkills = await installSkills(repoPath);
|
||||
if (installedSkills.length > 0) {
|
||||
createdFiles.push(`.claude/skills/gitnexus/ (${installedSkills.length} skills)`);
|
||||
}
|
||||
|
||||
return { files: createdFiles };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -310,6 +310,42 @@ async function upsertCodexConfigToml(configPath: string): Promise<void> {
|
|||
await fs.writeFile(configPath, `${nextContent.trimEnd()}\n`, 'utf-8');
|
||||
}
|
||||
|
||||
async function setupCodeBuddy(result: SetupResult): Promise<void> {
|
||||
const codebuddyDir = path.join(os.homedir(), '.codebuddy');
|
||||
if (!(await dirExists(codebuddyDir))) {
|
||||
result.skipped.push('CodeBuddy (not installed)');
|
||||
return;
|
||||
}
|
||||
|
||||
const mcpPath = path.join(codebuddyDir, 'mcp.json');
|
||||
try {
|
||||
const existing = await readJsonFile(mcpPath);
|
||||
const updated = mergeMcpConfig(existing);
|
||||
await writeJsonFile(mcpPath, updated);
|
||||
result.configured.push('CodeBuddy');
|
||||
} catch (err: any) {
|
||||
result.errors.push(`CodeBuddy: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function setupQoder(result: SetupResult): Promise<void> {
|
||||
const qoderDir = path.join(os.homedir(), '.qoder');
|
||||
if (!(await dirExists(qoderDir))) {
|
||||
result.skipped.push('Qoder (not installed)');
|
||||
return;
|
||||
}
|
||||
|
||||
const mcpPath = path.join(qoderDir, 'mcp.json');
|
||||
try {
|
||||
const existing = await readJsonFile(mcpPath);
|
||||
const updated = mergeMcpConfig(existing);
|
||||
await writeJsonFile(mcpPath, updated);
|
||||
result.configured.push('Qoder');
|
||||
} catch (err: any) {
|
||||
result.errors.push(`Qoder: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function setupCodex(result: SetupResult): Promise<void> {
|
||||
const codexDir = path.join(os.homedir(), '.codex');
|
||||
if (!(await dirExists(codexDir))) {
|
||||
|
|
@ -453,6 +489,44 @@ async function installOpenCodeSkills(result: SetupResult): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install global CodeBuddy skills to ~/.codebuddy/skills/
|
||||
*/
|
||||
async function installCodeBuddySkills(result: SetupResult): Promise<void> {
|
||||
const codebuddyDir = path.join(os.homedir(), '.codebuddy');
|
||||
if (!(await dirExists(codebuddyDir))) return;
|
||||
|
||||
const skillsDir = path.join(codebuddyDir, 'skills');
|
||||
try {
|
||||
const installed = await installSkillsTo(skillsDir);
|
||||
if (installed.length > 0) {
|
||||
result.configured.push(
|
||||
`CodeBuddy skills (${installed.length} skills → ~/.codebuddy/skills/)`,
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
result.errors.push(`CodeBuddy skills: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install global Qoder skills to ~/.qoder/skills/
|
||||
*/
|
||||
async function installQoderSkills(result: SetupResult): Promise<void> {
|
||||
const qoderDir = path.join(os.homedir(), '.qoder');
|
||||
if (!(await dirExists(qoderDir))) return;
|
||||
|
||||
const skillsDir = path.join(qoderDir, 'skills');
|
||||
try {
|
||||
const installed = await installSkillsTo(skillsDir);
|
||||
if (installed.length > 0) {
|
||||
result.configured.push(`Qoder skills (${installed.length} skills → ~/.qoder/skills/)`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
result.errors.push(`Qoder skills: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install global Codex skills to ~/.agents/skills/gitnexus/
|
||||
*/
|
||||
|
|
@ -493,6 +567,8 @@ export const setupCommand = async () => {
|
|||
await setupCursor(result);
|
||||
await setupClaudeCode(result);
|
||||
await setupOpenCode(result);
|
||||
await setupCodeBuddy(result);
|
||||
await setupQoder(result);
|
||||
await setupCodex(result);
|
||||
|
||||
// Install global skills for platforms that support them
|
||||
|
|
@ -500,6 +576,8 @@ export const setupCommand = async () => {
|
|||
await installClaudeCodeHooks(result);
|
||||
await installCursorSkills(result);
|
||||
await installOpenCodeSkills(result);
|
||||
await installCodeBuddySkills(result);
|
||||
await installQoderSkills(result);
|
||||
await installCodexSkills(result);
|
||||
|
||||
// Print results
|
||||
|
|
|
|||
|
|
@ -123,18 +123,26 @@ describe('generateAIContextFiles', () => {
|
|||
expect(starts).toBe(1);
|
||||
});
|
||||
|
||||
it('installs skills files', async () => {
|
||||
it('does not install skills to .claude/skills/gitnexus/', async () => {
|
||||
const stats = { nodes: 10 };
|
||||
await generateAIContextFiles(tmpDir, storagePath, 'TestProject', stats);
|
||||
|
||||
// Should NOT create .claude/skills/gitnexus/ during analyze
|
||||
const skillsDir = path.join(tmpDir, '.claude', 'skills', 'gitnexus');
|
||||
const exists = await fs
|
||||
.stat(skillsDir)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
expect(exists).toBe(false);
|
||||
});
|
||||
|
||||
it('does not install skills to ~/.codebuddy/skills/ or ~/.qoder/skills/', async () => {
|
||||
const stats = { nodes: 10 };
|
||||
const result = await generateAIContextFiles(tmpDir, storagePath, 'TestProject', stats);
|
||||
|
||||
// Should have installed skill files
|
||||
const skillsDir = path.join(tmpDir, '.claude', 'skills', 'gitnexus');
|
||||
try {
|
||||
const entries = await fs.readdir(skillsDir, { recursive: true });
|
||||
expect(entries.length).toBeGreaterThan(0);
|
||||
} catch {
|
||||
// Skills dir may not be created if skills source doesn't exist in test context
|
||||
}
|
||||
// analyze should not install any skills
|
||||
expect(result.files.find((f) => f.includes('.codebuddy/skills/'))).toBeUndefined();
|
||||
expect(result.files.find((f) => f.includes('.qoder/skills/'))).toBeUndefined();
|
||||
});
|
||||
|
||||
it('preserves manual AGENTS.md and CLAUDE.md edits when skipAgentsMd is enabled', async () => {
|
||||
|
|
|
|||
|
|
@ -192,3 +192,231 @@ describe('setupClaudeCode', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setupCodeBuddy', () => {
|
||||
let tempHome: string;
|
||||
let originalHome: string | undefined;
|
||||
let originalUserProfile: string | undefined;
|
||||
let platformDescriptor: PropertyDescriptor | undefined;
|
||||
|
||||
const setPlatform = (value: NodeJS.Platform) => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value,
|
||||
configurable: true,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
|
||||
originalHome = process.env.HOME;
|
||||
originalUserProfile = process.env.USERPROFILE;
|
||||
tempHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-codebuddy-setup-'));
|
||||
process.env.HOME = tempHome;
|
||||
process.env.USERPROFILE = tempHome;
|
||||
|
||||
// Create ~/.codebuddy for both MCP config and skills
|
||||
await fs.mkdir(path.join(tempHome, '.codebuddy'), { recursive: true });
|
||||
|
||||
platformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
|
||||
if (platformDescriptor) {
|
||||
Object.defineProperty(process, 'platform', platformDescriptor);
|
||||
}
|
||||
|
||||
process.env.HOME = originalHome;
|
||||
process.env.USERPROFILE = originalUserProfile;
|
||||
await fs.rm(tempHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('writes MCP config to ~/.codebuddy/mcp.json', async () => {
|
||||
setPlatform('linux');
|
||||
|
||||
const { setupCommand } = await import('../../src/cli/setup.js');
|
||||
await setupCommand();
|
||||
|
||||
const raw = await fs.readFile(path.join(tempHome, '.codebuddy', 'mcp.json'), 'utf-8');
|
||||
const config = JSON.parse(raw);
|
||||
|
||||
expect(config.mcpServers.gitnexus).toBeDefined();
|
||||
});
|
||||
|
||||
it('skips when ~/.codebuddy directory does not exist', async () => {
|
||||
await fs.rm(path.join(tempHome, '.codebuddy'), { recursive: true, force: true });
|
||||
|
||||
const { setupCommand } = await import('../../src/cli/setup.js');
|
||||
await setupCommand();
|
||||
|
||||
await expect(fs.access(path.join(tempHome, '.codebuddy', 'mcp.json'))).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('preserves existing config in ~/.codebuddy/mcp.json', async () => {
|
||||
setPlatform('linux');
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(tempHome, '.codebuddy', 'mcp.json'),
|
||||
JSON.stringify({ existingKey: 'keep-me', mcpServers: { other: { command: 'foo' } } }),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const { setupCommand } = await import('../../src/cli/setup.js');
|
||||
await setupCommand();
|
||||
|
||||
const raw = await fs.readFile(path.join(tempHome, '.codebuddy', 'mcp.json'), 'utf-8');
|
||||
const config = JSON.parse(raw);
|
||||
|
||||
expect(config.existingKey).toBe('keep-me');
|
||||
expect(config.mcpServers.other).toEqual({ command: 'foo' });
|
||||
expect(config.mcpServers.gitnexus).toBeDefined();
|
||||
});
|
||||
|
||||
it('installs skills to ~/.codebuddy/skills/', async () => {
|
||||
setPlatform('linux');
|
||||
|
||||
const { setupCommand } = await import('../../src/cli/setup.js');
|
||||
await setupCommand();
|
||||
|
||||
const skillsDir = path.join(tempHome, '.codebuddy', 'skills');
|
||||
const exists = await fs
|
||||
.stat(skillsDir)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
expect(exists).toBe(true);
|
||||
});
|
||||
|
||||
it('skips skills when ~/.codebuddy/ does not exist', async () => {
|
||||
setPlatform('linux');
|
||||
|
||||
await fs.rm(path.join(tempHome, '.codebuddy'), { recursive: true, force: true });
|
||||
|
||||
const { setupCommand } = await import('../../src/cli/setup.js');
|
||||
await setupCommand();
|
||||
|
||||
const skillsDir = path.join(tempHome, '.codebuddy', 'skills');
|
||||
const exists = await fs
|
||||
.stat(skillsDir)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
expect(exists).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setupQoder', () => {
|
||||
let tempHome: string;
|
||||
let originalHome: string | undefined;
|
||||
let originalUserProfile: string | undefined;
|
||||
let platformDescriptor: PropertyDescriptor | undefined;
|
||||
|
||||
const setPlatform = (value: NodeJS.Platform) => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value,
|
||||
configurable: true,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
|
||||
originalHome = process.env.HOME;
|
||||
originalUserProfile = process.env.USERPROFILE;
|
||||
tempHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-qoder-setup-'));
|
||||
process.env.HOME = tempHome;
|
||||
process.env.USERPROFILE = tempHome;
|
||||
|
||||
// Create ~/.qoder for both MCP config and skills
|
||||
await fs.mkdir(path.join(tempHome, '.qoder'), { recursive: true });
|
||||
|
||||
platformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
|
||||
if (platformDescriptor) {
|
||||
Object.defineProperty(process, 'platform', platformDescriptor);
|
||||
}
|
||||
|
||||
process.env.HOME = originalHome;
|
||||
process.env.USERPROFILE = originalUserProfile;
|
||||
await fs.rm(tempHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('writes MCP config to ~/.qoder/mcp.json', async () => {
|
||||
setPlatform('linux');
|
||||
|
||||
const { setupCommand } = await import('../../src/cli/setup.js');
|
||||
await setupCommand();
|
||||
|
||||
const raw = await fs.readFile(path.join(tempHome, '.qoder', 'mcp.json'), 'utf-8');
|
||||
const config = JSON.parse(raw);
|
||||
|
||||
expect(config.mcpServers.gitnexus).toBeDefined();
|
||||
});
|
||||
|
||||
it('skips when ~/.qoder directory does not exist', async () => {
|
||||
await fs.rm(path.join(tempHome, '.qoder'), { recursive: true, force: true });
|
||||
|
||||
const { setupCommand } = await import('../../src/cli/setup.js');
|
||||
await setupCommand();
|
||||
|
||||
await expect(fs.access(path.join(tempHome, '.qoder', 'mcp.json'))).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('preserves existing config in ~/.qoder/mcp.json', async () => {
|
||||
setPlatform('linux');
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(tempHome, '.qoder', 'mcp.json'),
|
||||
JSON.stringify({ existingKey: 'keep-me', mcpServers: { other: { command: 'foo' } } }),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const { setupCommand } = await import('../../src/cli/setup.js');
|
||||
await setupCommand();
|
||||
|
||||
const raw = await fs.readFile(path.join(tempHome, '.qoder', 'mcp.json'), 'utf-8');
|
||||
const config = JSON.parse(raw);
|
||||
|
||||
expect(config.existingKey).toBe('keep-me');
|
||||
expect(config.mcpServers.other).toEqual({ command: 'foo' });
|
||||
expect(config.mcpServers.gitnexus).toBeDefined();
|
||||
});
|
||||
|
||||
it('installs skills to ~/.qoder/skills/', async () => {
|
||||
setPlatform('linux');
|
||||
|
||||
const { setupCommand } = await import('../../src/cli/setup.js');
|
||||
await setupCommand();
|
||||
|
||||
const skillsDir = path.join(tempHome, '.qoder', 'skills');
|
||||
const exists = await fs
|
||||
.stat(skillsDir)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
expect(exists).toBe(true);
|
||||
});
|
||||
|
||||
it('skips skills when ~/.qoder/ does not exist', async () => {
|
||||
setPlatform('linux');
|
||||
|
||||
await fs.rm(path.join(tempHome, '.qoder'), { recursive: true, force: true });
|
||||
|
||||
const { setupCommand } = await import('../../src/cli/setup.js');
|
||||
await setupCommand();
|
||||
|
||||
const skillsDir = path.join(tempHome, '.qoder', 'skills');
|
||||
const exists = await fs
|
||||
.stat(skillsDir)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
expect(exists).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue