mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 13:22:35 -07:00
fix: use directory junctions on Windows for skill linking (#24823)
This commit is contained in:
@@ -26,66 +26,49 @@ describe('skillUtils', () => {
|
|||||||
vi.unstubAllEnvs();
|
vi.unstubAllEnvs();
|
||||||
});
|
});
|
||||||
|
|
||||||
const itif = (condition: boolean) => (condition ? it : it.skip);
|
|
||||||
|
|
||||||
describe('linkSkill', () => {
|
describe('linkSkill', () => {
|
||||||
// TODO: issue 19388 - Enable linkSkill tests on Windows
|
it('should successfully link from a local directory', async () => {
|
||||||
itif(process.platform !== 'win32')(
|
// Create a mock skill directory
|
||||||
'should successfully link from a local directory',
|
const mockSkillSourceDir = path.join(tempDir, 'mock-skill-source');
|
||||||
async () => {
|
const skillSubDir = path.join(mockSkillSourceDir, 'test-skill');
|
||||||
// Create a mock skill directory
|
await fs.mkdir(skillSubDir, { recursive: true });
|
||||||
const mockSkillSourceDir = path.join(tempDir, 'mock-skill-source');
|
await fs.writeFile(
|
||||||
const skillSubDir = path.join(mockSkillSourceDir, 'test-skill');
|
path.join(skillSubDir, 'SKILL.md'),
|
||||||
await fs.mkdir(skillSubDir, { recursive: true });
|
'---\nname: test-skill\ndescription: test\n---\nbody',
|
||||||
await fs.writeFile(
|
);
|
||||||
path.join(skillSubDir, 'SKILL.md'),
|
|
||||||
'---\nname: test-skill\ndescription: test\n---\nbody',
|
|
||||||
);
|
|
||||||
|
|
||||||
const skills = await linkSkill(
|
const skills = await linkSkill(mockSkillSourceDir, 'workspace', () => {});
|
||||||
mockSkillSourceDir,
|
expect(skills.length).toBe(1);
|
||||||
'workspace',
|
expect(skills[0].name).toBe('test-skill');
|
||||||
() => {},
|
|
||||||
);
|
|
||||||
expect(skills.length).toBe(1);
|
|
||||||
expect(skills[0].name).toBe('test-skill');
|
|
||||||
|
|
||||||
const linkedPath = path.join(tempDir, '.gemini/skills', 'test-skill');
|
const linkedPath = path.join(tempDir, '.gemini/skills', 'test-skill');
|
||||||
const stats = await fs.lstat(linkedPath);
|
const stats = await fs.lstat(linkedPath);
|
||||||
expect(stats.isSymbolicLink()).toBe(true);
|
expect(stats.isSymbolicLink()).toBe(true);
|
||||||
|
|
||||||
const linkTarget = await fs.readlink(linkedPath);
|
const linkTarget = await fs.readlink(linkedPath);
|
||||||
expect(path.resolve(linkTarget)).toBe(path.resolve(skillSubDir));
|
expect(path.resolve(linkTarget)).toBe(path.resolve(skillSubDir));
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
itif(process.platform !== 'win32')(
|
it('should overwrite existing skill at destination', async () => {
|
||||||
'should overwrite existing skill at destination',
|
const mockSkillSourceDir = path.join(tempDir, 'mock-skill-source');
|
||||||
async () => {
|
const skillSubDir = path.join(mockSkillSourceDir, 'test-skill');
|
||||||
const mockSkillSourceDir = path.join(tempDir, 'mock-skill-source');
|
await fs.mkdir(skillSubDir, { recursive: true });
|
||||||
const skillSubDir = path.join(mockSkillSourceDir, 'test-skill');
|
await fs.writeFile(
|
||||||
await fs.mkdir(skillSubDir, { recursive: true });
|
path.join(skillSubDir, 'SKILL.md'),
|
||||||
await fs.writeFile(
|
'---\nname: test-skill\ndescription: test\n---\nbody',
|
||||||
path.join(skillSubDir, 'SKILL.md'),
|
);
|
||||||
'---\nname: test-skill\ndescription: test\n---\nbody',
|
|
||||||
);
|
|
||||||
|
|
||||||
const targetDir = path.join(tempDir, '.gemini/skills');
|
const targetDir = path.join(tempDir, '.gemini/skills');
|
||||||
await fs.mkdir(targetDir, { recursive: true });
|
await fs.mkdir(targetDir, { recursive: true });
|
||||||
const existingPath = path.join(targetDir, 'test-skill');
|
const existingPath = path.join(targetDir, 'test-skill');
|
||||||
await fs.mkdir(existingPath);
|
await fs.mkdir(existingPath);
|
||||||
|
|
||||||
const skills = await linkSkill(
|
const skills = await linkSkill(mockSkillSourceDir, 'workspace', () => {});
|
||||||
mockSkillSourceDir,
|
expect(skills.length).toBe(1);
|
||||||
'workspace',
|
|
||||||
() => {},
|
|
||||||
);
|
|
||||||
expect(skills.length).toBe(1);
|
|
||||||
|
|
||||||
const stats = await fs.lstat(existingPath);
|
const stats = await fs.lstat(existingPath);
|
||||||
expect(stats.isSymbolicLink()).toBe(true);
|
expect(stats.isSymbolicLink()).toBe(true);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
it('should abort linking if consent is rejected', async () => {
|
it('should abort linking if consent is rejected', async () => {
|
||||||
const mockSkillSourceDir = path.join(tempDir, 'mock-skill-source');
|
const mockSkillSourceDir = path.join(tempDir, 'mock-skill-source');
|
||||||
@@ -237,39 +220,40 @@ describe('skillUtils', () => {
|
|||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
itif(process.platform !== 'win32')(
|
it('should successfully uninstall a skill even if its name was updated after linking', async () => {
|
||||||
'should successfully uninstall a skill even if its name was updated after linking',
|
// 1. Create source skill
|
||||||
async () => {
|
const sourceDir = path.join(tempDir, 'source-skill');
|
||||||
// 1. Create source skill
|
await fs.mkdir(sourceDir, { recursive: true });
|
||||||
const sourceDir = path.join(tempDir, 'source-skill');
|
const skillMdPath = path.join(sourceDir, 'SKILL.md');
|
||||||
await fs.mkdir(sourceDir, { recursive: true });
|
await fs.writeFile(
|
||||||
const skillMdPath = path.join(sourceDir, 'SKILL.md');
|
skillMdPath,
|
||||||
await fs.writeFile(
|
'---\nname: original-name\ndescription: test\n---\nbody',
|
||||||
skillMdPath,
|
);
|
||||||
'---\nname: original-name\ndescription: test\n---\nbody',
|
|
||||||
);
|
|
||||||
|
|
||||||
// 2. Link it
|
// 2. Link it
|
||||||
const skillsDir = path.join(tempDir, '.gemini/skills');
|
const skillsDir = path.join(tempDir, '.gemini/skills');
|
||||||
await fs.mkdir(skillsDir, { recursive: true });
|
await fs.mkdir(skillsDir, { recursive: true });
|
||||||
const destPath = path.join(skillsDir, 'original-name');
|
const destPath = path.join(skillsDir, 'original-name');
|
||||||
await fs.symlink(sourceDir, destPath, 'dir');
|
await fs.symlink(
|
||||||
|
sourceDir,
|
||||||
|
destPath,
|
||||||
|
process.platform === 'win32' ? 'junction' : 'dir',
|
||||||
|
);
|
||||||
|
|
||||||
// 3. Update name in source
|
// 3. Update name in source
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
skillMdPath,
|
skillMdPath,
|
||||||
'---\nname: updated-name\ndescription: test\n---\nbody',
|
'---\nname: updated-name\ndescription: test\n---\nbody',
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4. Uninstall by NEW name (this is the bug fix)
|
// 4. Uninstall by NEW name (this is the bug fix)
|
||||||
const result = await uninstallSkill('updated-name', 'user');
|
const result = await uninstallSkill('updated-name', 'user');
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
expect(result?.location).toBe(destPath);
|
expect(result?.location).toBe(destPath);
|
||||||
|
|
||||||
const exists = await fs.lstat(destPath).catch(() => null);
|
const exists = await fs.lstat(destPath).catch(() => null);
|
||||||
expect(exists).toBeNull();
|
expect(exists).toBeNull();
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
it('should successfully uninstall a skill by directory name if metadata is missing (fallback)', async () => {
|
it('should successfully uninstall a skill by directory name if metadata is missing (fallback)', async () => {
|
||||||
const skillsDir = path.join(tempDir, '.gemini/skills');
|
const skillsDir = path.join(tempDir, '.gemini/skills');
|
||||||
|
|||||||
@@ -248,7 +248,13 @@ export async function linkSkill(
|
|||||||
await fs.rm(destPath, { recursive: true, force: true });
|
await fs.rm(destPath, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.symlink(skillSourceDir, destPath, 'dir');
|
// Use 'junction' on Windows to avoid EPERM errors — junctions don't
|
||||||
|
// require elevated privileges or Developer Mode (fixes #24816)
|
||||||
|
await fs.symlink(
|
||||||
|
skillSourceDir,
|
||||||
|
destPath,
|
||||||
|
process.platform === 'win32' ? 'junction' : 'dir',
|
||||||
|
);
|
||||||
linkedSkills.push({ name: skillName, location: destPath });
|
linkedSkills.push({ name: skillName, location: destPath });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user