mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-13 08:51:28 +00:00
fix(cli): dynamically discover and install agent skills (#270)
This commit is contained in:
parent
3dbe08fab6
commit
649ad80dbb
2 changed files with 112 additions and 15 deletions
|
|
@ -10,6 +10,7 @@ import fs from 'fs/promises';
|
|||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { glob } from 'glob';
|
||||
import { getGlobalDir } from '../storage/repo-manager.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
|
|
@ -240,8 +241,6 @@ async function setupOpenCode(result: SetupResult): Promise<void> {
|
|||
|
||||
// ─── Skill Installation ───────────────────────────────────────────
|
||||
|
||||
const SKILL_NAMES = ['gitnexus-exploring', 'gitnexus-debugging', 'gitnexus-impact-analysis', 'gitnexus-refactoring', 'gitnexus-guide', 'gitnexus-cli'];
|
||||
|
||||
/**
|
||||
* Install GitNexus skills to a target directory.
|
||||
* Each skill is installed as {targetDir}/gitnexus-{skillName}/SKILL.md
|
||||
|
|
@ -255,25 +254,38 @@ async function installSkillsTo(targetDir: string): Promise<string[]> {
|
|||
const installed: string[] = [];
|
||||
const skillsRoot = path.join(__dirname, '..', '..', 'skills');
|
||||
|
||||
for (const skillName of SKILL_NAMES) {
|
||||
let flatFiles: string[] = [];
|
||||
let dirSkillFiles: string[] = [];
|
||||
try {
|
||||
[flatFiles, dirSkillFiles] = await Promise.all([
|
||||
glob('*.md', { cwd: skillsRoot }),
|
||||
glob('*/SKILL.md', { cwd: skillsRoot }),
|
||||
]);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const skillSources = new Map<string, { isDirectory: boolean }>();
|
||||
|
||||
for (const relPath of dirSkillFiles) {
|
||||
skillSources.set(path.dirname(relPath), { isDirectory: true });
|
||||
}
|
||||
for (const relPath of flatFiles) {
|
||||
const skillName = path.basename(relPath, '.md');
|
||||
if (!skillSources.has(skillName)) {
|
||||
skillSources.set(skillName, { isDirectory: false });
|
||||
}
|
||||
}
|
||||
|
||||
for (const [skillName, source] of skillSources) {
|
||||
const skillDir = path.join(targetDir, skillName);
|
||||
|
||||
try {
|
||||
// Try directory-based skill first (skills/{name}/SKILL.md)
|
||||
const dirSource = path.join(skillsRoot, skillName);
|
||||
const dirSkillFile = path.join(dirSource, 'SKILL.md');
|
||||
|
||||
let isDirectory = false;
|
||||
try {
|
||||
const stat = await fs.stat(dirSource);
|
||||
isDirectory = stat.isDirectory();
|
||||
} catch { /* not a directory */ }
|
||||
|
||||
if (isDirectory) {
|
||||
if (source.isDirectory) {
|
||||
const dirSource = path.join(skillsRoot, skillName);
|
||||
await copyDirRecursive(dirSource, skillDir);
|
||||
installed.push(skillName);
|
||||
} else {
|
||||
// Fall back to flat file (skills/{name}.md)
|
||||
const flatSource = path.join(skillsRoot, `${skillName}.md`);
|
||||
const content = await fs.readFile(flatSource, 'utf-8');
|
||||
await fs.mkdir(skillDir, { recursive: true });
|
||||
|
|
|
|||
85
gitnexus/test/integration/setup-skills.test.ts
Normal file
85
gitnexus/test/integration/setup-skills.test.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { setupCommand } from '../../src/cli/setup.js';
|
||||
|
||||
describe('setupCommand skills integration', () => {
|
||||
let tempHome: string;
|
||||
const originalHome = process.env.HOME;
|
||||
const testId = `${Date.now()}-${process.pid}`;
|
||||
const flatSkillName = `test-flat-skill-${testId}`;
|
||||
const dirSkillName = `test-dir-skill-${testId}`;
|
||||
const testDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const packageSkillsRoot = path.resolve(testDir, '..', '..', 'skills');
|
||||
|
||||
beforeAll(async () => {
|
||||
tempHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-setup-home-'));
|
||||
process.env.HOME = tempHome;
|
||||
await fs.mkdir(path.join(tempHome, '.cursor'), { recursive: true });
|
||||
|
||||
// Create temporary source skills to verify both supported source layouts:
|
||||
// - flat file: skills/{name}.md
|
||||
// - directory: skills/{name}/SKILL.md (+ nested files copied recursively)
|
||||
await fs.writeFile(
|
||||
path.join(packageSkillsRoot, `${flatSkillName}.md`),
|
||||
`---\nname: ${flatSkillName}\ndescription: temp flat skill\n---\n\n# Flat Test Skill`,
|
||||
'utf-8',
|
||||
);
|
||||
await fs.mkdir(path.join(packageSkillsRoot, dirSkillName, 'references'), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(packageSkillsRoot, dirSkillName, 'SKILL.md'),
|
||||
`---\nname: ${dirSkillName}\ndescription: temp directory skill\n---\n\n# Directory Test Skill`,
|
||||
'utf-8',
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(packageSkillsRoot, dirSkillName, 'references', 'note.md'),
|
||||
'# Directory Nested File',
|
||||
'utf-8',
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await fs.rm(path.join(packageSkillsRoot, `${flatSkillName}.md`), { force: true });
|
||||
await fs.rm(path.join(packageSkillsRoot, dirSkillName), { recursive: true, force: true });
|
||||
process.env.HOME = originalHome;
|
||||
await fs.rm(tempHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('installs packaged, flat-file, and directory skills into cursor skills directory', async () => {
|
||||
await setupCommand();
|
||||
|
||||
const cursorSkillsRoot = path.join(tempHome, '.cursor', 'skills');
|
||||
const entries = await fs.readdir(cursorSkillsRoot, { withFileTypes: true });
|
||||
const skillDirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
||||
|
||||
expect(skillDirs.length).toBeGreaterThan(0);
|
||||
expect(skillDirs).toContain('gitnexus-cli');
|
||||
|
||||
const skillContent = await fs.readFile(
|
||||
path.join(cursorSkillsRoot, 'gitnexus-cli', 'SKILL.md'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(skillContent).toContain('GitNexus CLI Commands');
|
||||
|
||||
// Flat file source should be installed as {name}/SKILL.md.
|
||||
const flatInstalled = await fs.readFile(
|
||||
path.join(cursorSkillsRoot, flatSkillName, 'SKILL.md'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(flatInstalled).toContain('# Flat Test Skill');
|
||||
|
||||
// Directory source should be copied recursively with nested files preserved.
|
||||
const dirInstalled = await fs.readFile(
|
||||
path.join(cursorSkillsRoot, dirSkillName, 'SKILL.md'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(dirInstalled).toContain('# Directory Test Skill');
|
||||
const nestedInstalled = await fs.readFile(
|
||||
path.join(cursorSkillsRoot, dirSkillName, 'references', 'note.md'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(nestedInstalled).toContain('Directory Nested File');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue