mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-28 15:25:27 +08:00
fix(skills): overwrite existing installations
Replace an existing skill directory instead of rejecting a duplicate installation. Remove stale supporting files so the installed directory exactly matches the new upload.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { mkdir, stat, writeFile } from 'node:fs/promises';
|
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
||||||
|
|
||||||
import type { IProviderSkills } from '@/shared/interfaces.js';
|
import type { IProviderSkills } from '@/shared/interfaces.js';
|
||||||
import type {
|
import type {
|
||||||
@@ -44,19 +44,6 @@ type PendingSkillInstall = {
|
|||||||
skill: ProviderSkill;
|
skill: ProviderSkill;
|
||||||
};
|
};
|
||||||
|
|
||||||
const pathExists = async (targetPath: string): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
await stat(targetPath);
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolveSkillSupportingFilePath = (
|
const resolveSkillSupportingFilePath = (
|
||||||
skillDirectoryPath: string,
|
skillDirectoryPath: string,
|
||||||
relativePath: string,
|
relativePath: string,
|
||||||
@@ -196,13 +183,6 @@ export abstract class SkillsProvider implements IProviderSkills {
|
|||||||
}
|
}
|
||||||
|
|
||||||
seenSkillPaths.add(normalizedSkillPath);
|
seenSkillPaths.add(normalizedSkillPath);
|
||||||
if (await pathExists(skillDirectoryPath)) {
|
|
||||||
throw new AppError(`Skill target "${resolvedDirectoryName}" already exists.`, {
|
|
||||||
code: 'PROVIDER_SKILL_ALREADY_EXISTS',
|
|
||||||
statusCode: 409,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const supportingFiles = (entry.files ?? []).map((file) => ({
|
const supportingFiles = (entry.files ?? []).map((file) => ({
|
||||||
targetPath: resolveSkillSupportingFilePath(skillDirectoryPath, file.relativePath, index),
|
targetPath: resolveSkillSupportingFilePath(skillDirectoryPath, file.relativePath, index),
|
||||||
content: file.encoding === 'base64'
|
content: file.encoding === 'base64'
|
||||||
@@ -243,6 +223,8 @@ export abstract class SkillsProvider implements IProviderSkills {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const install of pendingInstalls) {
|
for (const install of pendingInstalls) {
|
||||||
|
// Replace the complete skill directory so removed scripts or assets do not remain stale.
|
||||||
|
await rm(install.skillDirectoryPath, { recursive: true, force: true });
|
||||||
await mkdir(install.skillDirectoryPath, { recursive: true });
|
await mkdir(install.skillDirectoryPath, { recursive: true });
|
||||||
await writeFile(install.skillPath, `${install.content}\n`, 'utf8');
|
await writeFile(install.skillPath, `${install.content}\n`, 'utf8');
|
||||||
for (const file of install.supportingFiles) {
|
for (const file of install.supportingFiles) {
|
||||||
|
|||||||
@@ -585,18 +585,20 @@ test('providerSkillsService adds global skills for claude, codex, gemini, and cu
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const replacedCodexSkills = await providerSkillsService.addProviderSkills('codex', {
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
directoryName: 'uploaded-codex-folder',
|
||||||
|
content: '---\nname: replacement\ndescription: Replacement skill\n---\n\nReplacement body.\n',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.equal(replacedCodexSkills[0]?.command, '$replacement');
|
||||||
|
assert.match(await fs.readFile(createdCodexSkill.sourcePath, 'utf8'), /Replacement body\./);
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
providerSkillsService.addProviderSkills('codex', {
|
fs.stat(path.join(path.dirname(createdCodexSkill.sourcePath), 'scripts', 'run.js')),
|
||||||
entries: [
|
{ code: 'ENOENT' },
|
||||||
{
|
|
||||||
directoryName: 'uploaded-codex-folder',
|
|
||||||
content: '---\nname: replacement\n---\n\nReplacement body.\n',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
/already exists/i,
|
|
||||||
);
|
);
|
||||||
assert.match(await fs.readFile(createdCodexSkill.sourcePath, 'utf8'), /Codex body\./);
|
|
||||||
|
|
||||||
const pendingBatchSkillPath = path.join(tempRoot, '.agents', 'skills', 'pending-batch', 'SKILL.md');
|
const pendingBatchSkillPath = path.join(tempRoot, '.agents', 'skills', 'pending-batch', 'SKILL.md');
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
@@ -652,7 +654,7 @@ test('providerSkillsService adds global skills for claude, codex, gemini, and cu
|
|||||||
assert.equal(listedClaudeSkills.some((skill) => skill.name === 'claude-global'), true);
|
assert.equal(listedClaudeSkills.some((skill) => skill.name === 'claude-global'), true);
|
||||||
|
|
||||||
const listedCodexSkills = await providerSkillsService.listProviderSkills('codex');
|
const listedCodexSkills = await providerSkillsService.listProviderSkills('codex');
|
||||||
assert.equal(listedCodexSkills.some((skill) => skill.name === 'codex-global'), true);
|
assert.equal(listedCodexSkills.some((skill) => skill.name === 'replacement'), true);
|
||||||
|
|
||||||
const listedGeminiSkills = await providerSkillsService.listProviderSkills('gemini');
|
const listedGeminiSkills = await providerSkillsService.listProviderSkills('gemini');
|
||||||
assert.equal(listedGeminiSkills.some((skill) => skill.name === 'gemini-global'), true);
|
assert.equal(listedGeminiSkills.some((skill) => skill.name === 'gemini-global'), true);
|
||||||
|
|||||||
Reference in New Issue
Block a user