Fix/skill zip symlink resolution (#1040)

* fix(skill): resolve symlinks in ZIP extraction for GitHub repos (#1001)

- detect symlink entries via is_symlink() during ZIP extraction and collect target paths

- add resolve_symlinks_in_dir() to copy symlink target content into link location

- canonicalize base_dir to fix macOS /tmp → /private/tmp path comparison issue

- add path traversal safety check to block symlinks pointing outside repo boundary

- apply symlink resolution to both download_and_extract and extract_local_zip paths

Closes https://github.com/farion1231/cc-switch/issues/1001

* fix(skill): change search to match name and repo instead of description

* feat(skill): support importing skills from ~/.agents/skills/ directory

- Scan ~/.agents/skills/ in scan_unmanaged() for skill discovery
- Parse ~/.agents/.skill-lock.json to extract repo owner/name metadata
- Auto-add discovered repos to skill_repos management on import
- Add path field to UnmanagedSkill to show discovered location in UI

Closes #980

* fix(skill): use metadata name or ZIP filename for root-level SKILL.md imports (#1000)

When a ZIP contains SKILL.md at the root without a wrapper directory,
the install name was derived from the temp directory name (e.g. .tmpDZKGpF).
Now falls back to SKILL.md frontmatter name, then ZIP filename stem.

* feat(skill): scan ~/.cc-switch/skills/ for unmanaged skill discovery and import

* refactor(skill): unify scan/import logic with lock file skillPath and repo saving

- Deduplicate scan_unmanaged and import_from_apps using shared source list
- Replace hand-written AppType match with as_str() and AppType::all()
- Extract read_skill_name_desc, build_repo_info_from_lock, save_repos_from_lock helpers
- Add SkillApps::from_labels for building enable state from source labels
- Parse skillPath from .skill-lock.json for correct readme URLs
- Save skill repos to skill_repos table in both import and migration paths

* fix(skill): resolve symlink and path traversal issues in ZIP skill import

* fix(skill): separate source path validation and add canonicalization for symlink safety
This commit is contained in:
Dex Miller
2026-02-15 20:57:14 +08:00
committed by GitHub
parent 7b20c17ea4
commit 508aa6070c
8 changed files with 574 additions and 171 deletions
+5 -7
View File
@@ -213,14 +213,12 @@ export const SkillsPage = forwardRef<SkillsPageHandle, SkillsPageProps>(
const query = searchQuery.toLowerCase();
return byStatus.filter((skill) => {
const name = skill.name?.toLowerCase() || "";
const description = skill.description?.toLowerCase() || "";
const directory = skill.directory?.toLowerCase() || "";
const repo =
skill.repoOwner && skill.repoName
? `${skill.repoOwner}/${skill.repoName}`.toLowerCase()
: "";
return (
name.includes(query) ||
description.includes(query) ||
directory.includes(query)
);
return name.includes(query) || repo.includes(query);
});
}, [skills, searchQuery, filterRepo, filterStatus]);
+6 -2
View File
@@ -306,6 +306,7 @@ interface ImportSkillsDialogProps {
name: string;
description?: string;
foundIn: string[];
path: string;
}>;
onImport: (directories: string[]) => void;
onClose: () => void;
@@ -362,8 +363,11 @@ const ImportSkillsDialog: React.FC<ImportSkillsDialogProps> = ({
{skill.description}
</div>
)}
<div className="text-xs text-muted-foreground/70 mt-1">
{t("skills.foundIn")}: {skill.foundIn.join(", ")}
<div
className="text-xs text-muted-foreground/50 mt-1 truncate"
title={skill.path}
>
{skill.path}
</div>
</div>
</label>
+1 -1
View File
@@ -1322,7 +1322,7 @@
"skillCount": "{{count}} skills detected"
},
"search": "Search Skills",
"searchPlaceholder": "Search skill name or description...",
"searchPlaceholder": "Search skill name or repo...",
"filter": {
"placeholder": "Filter by status",
"all": "All",
+1 -1
View File
@@ -1320,7 +1320,7 @@
"skillCount": "{{count}} 件のスキルを検出"
},
"search": "スキルを検索",
"searchPlaceholder": "スキル名または説明で検索...",
"searchPlaceholder": "スキル名またはリポジトリで検索...",
"filter": {
"placeholder": "状態で絞り込み",
"all": "すべて",
+1 -1
View File
@@ -1322,7 +1322,7 @@
"skillCount": "识别到 {{count}} 个技能"
},
"search": "搜索技能",
"searchPlaceholder": "搜索技能名称或描述...",
"searchPlaceholder": "搜索技能名称或仓库名称...",
"filter": {
"placeholder": "状态筛选",
"all": "全部",
+1
View File
@@ -45,6 +45,7 @@ export interface UnmanagedSkill {
name: string;
description?: string;
foundIn: string[];
path: string;
}
/** 技能对象(兼容旧 API) */