fix(extensions): sanitize suggested skill folder names
This commit is contained in:
parent
4578f11dd7
commit
86bc4ce4e8
5 changed files with 88 additions and 18 deletions
|
|
@ -32,6 +32,7 @@ import {
|
|||
readSkillTemplateContent,
|
||||
updateSkillTemplateFrontmatter,
|
||||
} from './skillDraftUtils';
|
||||
import { toSuggestedSkillFolderName } from './skillFolderNameUtils';
|
||||
import { resolveSkillProjectPath } from './skillProjectUtils';
|
||||
import { SkillReviewDialog } from './SkillReviewDialog';
|
||||
|
||||
|
|
@ -61,16 +62,6 @@ function parseInitialDescription(detail: SkillDetail | null): string {
|
|||
return detail?.item.description ?? '';
|
||||
}
|
||||
|
||||
function toSuggestedFolderName(value: string): string {
|
||||
return value
|
||||
.normalize('NFKD')
|
||||
.replace(/[^\x00-\x7F]/g, '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.slice(0, 80);
|
||||
}
|
||||
|
||||
export const SkillEditorDialog = ({
|
||||
open,
|
||||
mode,
|
||||
|
|
@ -192,7 +183,7 @@ export const SkillEditorDialog = ({
|
|||
notes: nextNotes,
|
||||
});
|
||||
const rawInput = readSkillTemplateContent(nextRawContent);
|
||||
const suggestedFolderName = toSuggestedFolderName(nextName || 'New Skill');
|
||||
const suggestedFolderName = toSuggestedSkillFolderName(nextName || 'New Skill');
|
||||
const hasCustomMarkdown = mode === 'edit' && rawInput.hasUnstructuredBody;
|
||||
|
||||
setScope(nextScope);
|
||||
|
|
@ -485,7 +476,7 @@ export const SkillEditorDialog = ({
|
|||
const nextValue = event.target.value;
|
||||
setName(nextValue);
|
||||
if (mode === 'create' && !folderNameEdited) {
|
||||
setFolderName(toSuggestedFolderName(nextValue || 'New Skill'));
|
||||
setFolderName(toSuggestedSkillFolderName(nextValue || 'New Skill'));
|
||||
}
|
||||
applyFormToRawContent({ name: nextValue });
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import {
|
|||
import { useStore } from '@renderer/store';
|
||||
import { FileSearch, FolderOpen, X } from 'lucide-react';
|
||||
|
||||
import { getSuggestedSkillFolderNameFromPath } from './skillFolderNameUtils';
|
||||
import { SkillReviewDialog } from './SkillReviewDialog';
|
||||
import { resolveSkillProjectPath } from './skillProjectUtils';
|
||||
|
||||
|
|
@ -57,11 +58,6 @@ interface SkillImportDialogProps {
|
|||
onImported: (skillId: string | null) => void;
|
||||
}
|
||||
|
||||
function getFolderNameFromPath(value: string): string {
|
||||
const segments = value.split(/[\\/]/u).filter(Boolean);
|
||||
return segments.at(-1) ?? '';
|
||||
}
|
||||
|
||||
export const SkillImportDialog = ({
|
||||
open,
|
||||
projectPath,
|
||||
|
|
@ -101,7 +97,7 @@ export const SkillImportDialog = ({
|
|||
if (!open || folderNameEdited) {
|
||||
return;
|
||||
}
|
||||
setFolderName(getFolderNameFromPath(sourceDir));
|
||||
setFolderName(getSuggestedSkillFolderNameFromPath(sourceDir));
|
||||
}, [folderNameEdited, open, sourceDir]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
export function toSuggestedSkillFolderName(value: string, fallback = 'new-skill'): string {
|
||||
const normalized = value
|
||||
.normalize('NFKD')
|
||||
.replace(/[^\x00-\x7F]/g, '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.slice(0, 80);
|
||||
|
||||
return normalized || fallback;
|
||||
}
|
||||
|
||||
export function getSuggestedSkillFolderNameFromPath(
|
||||
value: string,
|
||||
fallback = 'imported-skill'
|
||||
): string {
|
||||
const segments = value.split(/[\\/]/u).filter(Boolean);
|
||||
return toSuggestedSkillFolderName(segments.at(-1) ?? '', fallback);
|
||||
}
|
||||
|
|
@ -188,6 +188,44 @@ describe('SkillImportDialog', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('sanitizes the suggested destination folder when the source folder name is not CLI-safe', async () => {
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
|
||||
selectFoldersMock.mockResolvedValueOnce(['/tmp/My Skill Folder']);
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
React.createElement(SkillImportDialog, {
|
||||
open: true,
|
||||
projectPath: null,
|
||||
projectLabel: null,
|
||||
onClose: vi.fn(),
|
||||
onImported: vi.fn(),
|
||||
})
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const browseButton = Array.from(host.querySelectorAll('button')).find(
|
||||
(button) => button.textContent?.includes('Browse')
|
||||
) as HTMLButtonElement;
|
||||
|
||||
await act(async () => {
|
||||
browseButton.click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const folderInput = host.querySelector('#skill-import-folder') as HTMLInputElement;
|
||||
expect(folderInput.value).toBe('my-skill-folder');
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to user scope when the project context disappears mid-dialog', async () => {
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
getSuggestedSkillFolderNameFromPath,
|
||||
toSuggestedSkillFolderName,
|
||||
} from '../../../../../src/renderer/components/extensions/skills/skillFolderNameUtils';
|
||||
|
||||
describe('skillFolderNameUtils', () => {
|
||||
it('creates a safe slug from a human-readable skill name', () => {
|
||||
expect(toSuggestedSkillFolderName('Review Helper 2')).toBe('review-helper-2');
|
||||
});
|
||||
|
||||
it('falls back to a safe default when the name cannot be slugged', () => {
|
||||
expect(toSuggestedSkillFolderName('Привет мир')).toBe('new-skill');
|
||||
});
|
||||
|
||||
it('sanitizes imported folder names from the selected source path', () => {
|
||||
expect(getSuggestedSkillFolderNameFromPath('/tmp/My Skill Folder')).toBe(
|
||||
'my-skill-folder'
|
||||
);
|
||||
});
|
||||
|
||||
it('uses an import-specific fallback when the source folder name is unusable', () => {
|
||||
expect(getSuggestedSkillFolderNameFromPath('/tmp/技能')).toBe('imported-skill');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue