✨ feat(skills_linker): 添加一键安装脚本。
This commit is contained in:
1 parent
2ce3f0173d
commit
707a6f1ef1
3 files changed
+445
-3
No files matched your search
@@ -33,7 +33,7 @@
|
|||||||
- 🧩 **开箱即用**:每个技能自带完整提示词(`SKILL.md`)与接口策略(`agents/openai.yaml`),复制即用、无需额外依赖。
|
- 🧩 **开箱即用**:每个技能自带完整提示词(`SKILL.md`)与接口策略(`agents/openai.yaml`),复制即用、无需额外依赖。
|
||||||
- 🗂️ **分层分类**:按「开发与规范 → 测试与修复 → 交付与部署 → 全自动化」四层递进组织,形成完整质量闭环。
|
- 🗂️ **分层分类**:按「开发与规范 → 测试与修复 → 交付与部署 → 全自动化」四层递进组织,形成完整质量闭环。
|
||||||
- 🔗 **技能互链**:技能之间通过 `/技能名` 相互调用(如 `ci-cd-pipeline` 可串联 13 个子技能),可组装成自动化流水线。
|
- 🔗 **技能互链**:技能之间通过 `/技能名` 相互调用(如 `ci-cd-pipeline` 可串联 13 个子技能),可组装成自动化流水线。
|
||||||
- 🚀 **一键安装**:`skills_linker.sh` 将分类目录下全部技能软链接到 `~/.agents/skills/`,全局可用。
|
- 🚀 **一键安装**:`skills_linker.sh`(Linux / macOS)与 `skills_linker.bat`(Windows)将分类目录下全部技能链接到 `~/.agents/skills/`,全局可用。
|
||||||
- 📊 **可观测可审计**:每个技能输出带时间戳的结构化 Markdown 报告(`*_<timestamp>.md`),全程留痕、便于审计。
|
- 📊 **可观测可审计**:每个技能输出带时间戳的结构化 Markdown 报告(`*_<timestamp>.md`),全程留痕、便于审计。
|
||||||
- 🛡️ **安全优先**:内置强制安全规则——生产环境保护、敏感信息脱敏、最小权限、人工确认点。
|
- 🛡️ **安全优先**:内置强制安全规则——生产环境保护、敏感信息脱敏、最小权限、人工确认点。
|
||||||
|
|
||||||
@@ -43,7 +43,8 @@
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
AgentSkills/
|
AgentSkills/
|
||||||
├── skills_linker.sh # 一键安装脚本(软链接到 ~/.agents/skills/)
|
├── skills_linker.sh # 一键安装脚本 Linux/macOS(软链接到 ~/.agents/skills/)
|
||||||
|
├── skills_linker.bat # 一键安装脚本 Windows(junction / 符号链接)
|
||||||
├── README.md # 项目说明(本文件)
|
├── README.md # 项目说明(本文件)
|
||||||
├── development-and-standards/ # ① 开发与规范(6 个技能)
|
├── development-and-standards/ # ① 开发与规范(6 个技能)
|
||||||
│ ├── readme-generator/ # README 生成器
|
│ ├── readme-generator/ # README 生成器
|
||||||
@@ -115,6 +116,23 @@ chmod +x skills_linker.sh
|
|||||||
>
|
>
|
||||||
> `skills_linker.sh` 会跳过指向不同目标的同名已存在链接(打印警告),不会覆盖非符号链接目录,可安全重复执行。
|
> `skills_linker.sh` 会跳过指向不同目标的同名已存在链接(打印警告),不会覆盖非符号链接目录,可安全重复执行。
|
||||||
|
|
||||||
|
#### 🪟 Windows
|
||||||
|
|
||||||
|
在仓库根目录双击 `skills_linker.bat`,或在 cmd / PowerShell 中执行:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
skills_linker.bat :: 链接全部技能(默认目标 %USERPROFILE%\.agents\skills)
|
||||||
|
skills_linker.bat /n :: 预演,只打印计划,不写入不删除
|
||||||
|
skills_linker.bat /u :: 卸载,只删除指向本仓库的链接
|
||||||
|
skills_linker.bat /? :: 查看全部选项
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
>
|
||||||
|
> Windows 版优先使用目录联接(junction),无需管理员权限或开发者模式;不可用时自动回退到符号链接,网络路径请用 `/c` 复制模式。已存在且指向同一技能会跳过、指向别处会替换、同名普通目录只告警不覆盖,同样可安全重复执行。
|
||||||
|
>
|
||||||
|
> 脚本输出为纯 ASCII 英文:cmd.exe 按**当前控制台代码页**解码批处理文件,中文 Windows 的 936 控制台(含资源管理器双击)与 PowerShell 7 的 65001 控制台无法用同一份非 ASCII 字节同时正确显示,而纯 ASCII 在任何代码页下都一致。
|
||||||
|
|
||||||
### 3️⃣ 在 Agent 中调用
|
### 3️⃣ 在 Agent 中调用
|
||||||
|
|
||||||
在支持 Skills 的 AI 编码工具(如 Claude Code、VS Code Copilot Chat 等)中,直接以技能名唤起即可,例如 `/blackbox-tester`、`/ci-cd-pipeline`。
|
在支持 Skills 的 AI 编码工具(如 Claude Code、VS Code Copilot Chat 等)中,直接以技能名唤起即可,例如 `/blackbox-tester`、`/ci-cd-pipeline`。
|
||||||
@@ -247,7 +265,7 @@ flowchart LR
|
|||||||
1. 每个技能放在对应分类目录下,包含 `SKILL.md` 与 `agents/openai.yaml`。
|
1. 每个技能放在对应分类目录下,包含 `SKILL.md` 与 `agents/openai.yaml`。
|
||||||
2. `SKILL.md` 至少包含:角色与描述、工作流程、行为准则、输出格式。
|
2. `SKILL.md` 至少包含:角色与描述、工作流程、行为准则、输出格式。
|
||||||
3. 涉及破坏性或副作用操作的技能,务必在 `openai.yaml` 中设置 `require_confirmation: true`。
|
3. 涉及破坏性或副作用操作的技能,务必在 `openai.yaml` 中设置 `require_confirmation: true`。
|
||||||
4. 新增技能后,可运行 `./skills_linker.sh` 验证安装脚本兼容性。
|
4. 新增技能后,可运行 `./skills_linker.sh`(Windows 上运行 `skills_linker.bat /n` 预演)验证安装脚本兼容性。
|
||||||
|
|
||||||
<!-- TODO: 补充 CONTRIBUTING.md 与具体的贡献模板 -->
|
<!-- TODO: 补充 CONTRIBUTING.md 与具体的贡献模板 -->
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,424 @@
|
|||||||
|
@echo off
|
||||||
|
rem ===========================================================================
|
||||||
|
rem AgentSkills - skills linker for Windows (cmd / .bat version)
|
||||||
|
rem ---------------------------------------------------------------------------
|
||||||
|
rem This file is deliberately PURE ASCII, UTF-8/ANSI agnostic, CRLF endings.
|
||||||
|
rem
|
||||||
|
rem Why: cmd.exe decodes a batch file with the *current console code page*.
|
||||||
|
rem A 936 (Simplified-Chinese / Explorer double click) console and a 65001
|
||||||
|
rem (PowerShell 7) console decode the same non-ASCII bytes differently, and
|
||||||
|
rem switching the code page inside a batch file (chcp) can desynchronise cmd's
|
||||||
|
rem parser so that it executes comment text. A pure ASCII file has no such
|
||||||
|
rem ambiguity: every byte means the same thing under every code page, and the
|
||||||
|
rem output renders identically everywhere. Keep it ASCII when editing.
|
||||||
|
rem ===========================================================================
|
||||||
|
setlocal EnableExtensions DisableDelayedExpansion
|
||||||
|
|
||||||
|
rem ===========================================================================
|
||||||
|
rem AgentSkills skills linker (Windows) - the counterpart of skills_linker.sh
|
||||||
|
rem
|
||||||
|
rem What it does: scans <repo root>\<category>\<skill> two levels below this
|
||||||
|
rem script and links every skill directory into the global skills directory
|
||||||
|
rem (default %USERPROFILE%\.agents\skills) so every project can use them.
|
||||||
|
rem How it links: a directory junction first (no admin rights, no developer
|
||||||
|
rem mode needed), falling back to a directory symlink (mklink /D). Use
|
||||||
|
rem /c (copy mode) when links are not usable, e.g. on a network path.
|
||||||
|
rem Safety: it only adds or removes the links themselves inside the target
|
||||||
|
rem directory - it never enters or deletes the content behind a link, and it
|
||||||
|
rem never overwrites a real directory that happens to share a skill name.
|
||||||
|
rem ===========================================================================
|
||||||
|
|
||||||
|
set "EXITCODE=0"
|
||||||
|
set "ROOT=%~dp0"
|
||||||
|
if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%"
|
||||||
|
if "%ROOT:~-1%"==":" set "ROOT=%ROOT%\"
|
||||||
|
set "TARGET="
|
||||||
|
set "MODE=link"
|
||||||
|
set "DRYRUN=0"
|
||||||
|
set "PAUSE_AT_END=0"
|
||||||
|
set "TMPLIST="
|
||||||
|
set /a N_LINKED=0 >nul
|
||||||
|
set /a N_REPLACED=0 >nul
|
||||||
|
set /a N_REMOVED=0 >nul
|
||||||
|
set /a N_COPIED=0 >nul
|
||||||
|
set /a N_SKIPPED=0 >nul
|
||||||
|
set /a N_WARN=0 >nul
|
||||||
|
set /a N_FAIL=0 >nul
|
||||||
|
|
||||||
|
rem A double click starts us as: cmd /c ""<script>" " (no arguments, the path
|
||||||
|
rem wrapped in doubled quotes). Only that case waits for a key at the end, so
|
||||||
|
rem windows opened by Explorer do not vanish; every other call stays scriptable.
|
||||||
|
if not "%~1"=="" goto pause_checked
|
||||||
|
echo(%cmdcmdline% | findstr /c:"\"\"" >nul 2>&1 && set "PAUSE_AT_END=1"
|
||||||
|
:pause_checked
|
||||||
|
|
||||||
|
rem ------------------------------ parse arguments ------------------------------
|
||||||
|
:parse
|
||||||
|
if "%~1"=="" goto parsed
|
||||||
|
set "ARG=%~1"
|
||||||
|
if /i "%ARG%"=="/?" goto usage
|
||||||
|
if /i "%ARG%"=="-?" goto usage
|
||||||
|
if /i "%ARG%"=="/h" goto usage
|
||||||
|
if /i "%ARG%"=="-h" goto usage
|
||||||
|
if /i "%ARG%"=="--help" goto usage
|
||||||
|
if /i "%ARG%"=="/l" goto arg_shift
|
||||||
|
if /i "%ARG%"=="-l" goto arg_shift
|
||||||
|
if /i "%ARG%"=="--link" goto arg_shift
|
||||||
|
if /i "%ARG%"=="/c" goto opt_copy
|
||||||
|
if /i "%ARG%"=="-c" goto opt_copy
|
||||||
|
if /i "%ARG%"=="--copy" goto opt_copy
|
||||||
|
if /i "%ARG%"=="/u" goto opt_unlink
|
||||||
|
if /i "%ARG%"=="-u" goto opt_unlink
|
||||||
|
if /i "%ARG%"=="--unlink" goto opt_unlink
|
||||||
|
if /i "%ARG%"=="/n" goto opt_dry
|
||||||
|
if /i "%ARG%"=="-n" goto opt_dry
|
||||||
|
if /i "%ARG%"=="--dry-run" goto opt_dry
|
||||||
|
if /i "%ARG%"=="/t" goto opt_target
|
||||||
|
if /i "%ARG%"=="-t" goto opt_target
|
||||||
|
if /i "%ARG%"=="--target" goto opt_target
|
||||||
|
echo [FAIL] unknown option: "%ARG%"
|
||||||
|
call :show_usage
|
||||||
|
set "EXITCODE=2"
|
||||||
|
goto finish
|
||||||
|
|
||||||
|
:opt_copy
|
||||||
|
set "MODE=copy"
|
||||||
|
goto arg_shift
|
||||||
|
:opt_unlink
|
||||||
|
set "MODE=unlink"
|
||||||
|
goto arg_shift
|
||||||
|
:opt_dry
|
||||||
|
set "DRYRUN=1"
|
||||||
|
goto arg_shift
|
||||||
|
:opt_target
|
||||||
|
set "V=%~2"
|
||||||
|
if "%V%"=="" goto err_value
|
||||||
|
if "%V:~0,1%"=="/" goto err_value
|
||||||
|
if "%V:~0,1%"=="-" goto err_value
|
||||||
|
set "TARGET=%V%"
|
||||||
|
shift
|
||||||
|
goto arg_shift
|
||||||
|
:err_value
|
||||||
|
echo [FAIL] option %ARG% needs a directory value
|
||||||
|
call :show_usage
|
||||||
|
set "EXITCODE=2"
|
||||||
|
goto finish
|
||||||
|
:arg_shift
|
||||||
|
shift
|
||||||
|
goto parse
|
||||||
|
|
||||||
|
:parsed
|
||||||
|
if defined TARGET goto have_target
|
||||||
|
if not defined USERPROFILE goto err_nohome
|
||||||
|
set "TARGET=%USERPROFILE%\.agents\skills"
|
||||||
|
:have_target
|
||||||
|
for %%A in ("%TARGET%") do set "TARGET=%%~fA"
|
||||||
|
|
||||||
|
rem ------------------------------ banner ------------------------------
|
||||||
|
echo(
|
||||||
|
echo == AgentSkills skills linker ==
|
||||||
|
echo( repo root : "%ROOT%"
|
||||||
|
echo( target : "%TARGET%"
|
||||||
|
if /i "%MODE%"=="link" echo( mode : link (junction, falls back to a symlink)
|
||||||
|
if /i "%MODE%"=="copy" echo( mode : copy (real copies, no links)
|
||||||
|
if /i "%MODE%"=="unlink" echo( mode : unlink (removes only links into this repo)
|
||||||
|
if "%DRYRUN%"=="1" echo( dry run : yes (nothing is written or deleted)
|
||||||
|
echo(
|
||||||
|
|
||||||
|
rem ------------------------------ prepare target ------------------------------
|
||||||
|
if /i "%MODE%"=="unlink" goto pre_unlink
|
||||||
|
if "%DRYRUN%"=="1" goto scan
|
||||||
|
if exist "%TARGET%\" goto scan
|
||||||
|
mkdir "%TARGET%" >nul 2>&1
|
||||||
|
if not exist "%TARGET%\" goto fail_target
|
||||||
|
echo [DIR] created target directory "%TARGET%"
|
||||||
|
goto scan
|
||||||
|
|
||||||
|
:pre_unlink
|
||||||
|
if not exist "%TARGET%\" goto no_target
|
||||||
|
goto scan
|
||||||
|
|
||||||
|
:no_target
|
||||||
|
echo nothing to unlink: target directory "%TARGET%" does not exist
|
||||||
|
goto summary
|
||||||
|
|
||||||
|
rem ------------------------------ scan and process ------------------------------
|
||||||
|
:scan
|
||||||
|
if not defined TEMP set "TEMP=%TMP%"
|
||||||
|
if defined TEMP set "TMPLIST=%TEMP%\agentskills_seen_%RANDOM%%RANDOM%.txt"
|
||||||
|
if not defined TMPLIST set "TMPLIST=%ROOT%\agentskills_seen_%RANDOM%.txt"
|
||||||
|
type nul > "%TMPLIST%" 2>nul
|
||||||
|
for /f "delims=" %%C in ('dir /b /ad /on "%ROOT%" 2^>nul') do (
|
||||||
|
set "CAT=%%C"
|
||||||
|
call :each_category
|
||||||
|
)
|
||||||
|
goto summary
|
||||||
|
|
||||||
|
rem ------------------------------ summary ------------------------------
|
||||||
|
:summary
|
||||||
|
if defined TMPLIST del /f /q "%TMPLIST%" >nul 2>&1
|
||||||
|
echo(
|
||||||
|
echo ------------------------------ summary ------------------------------
|
||||||
|
echo( new links : %N_LINKED%
|
||||||
|
echo( replaced : %N_REPLACED%
|
||||||
|
echo( copied : %N_COPIED%
|
||||||
|
echo( removed : %N_REMOVED%
|
||||||
|
echo( skipped : %N_SKIPPED%
|
||||||
|
echo( warnings : %N_WARN%
|
||||||
|
echo( failures : %N_FAIL%
|
||||||
|
echo(
|
||||||
|
if "%DRYRUN%"=="1" echo (dry run: the lines above are the plan, nothing was changed)
|
||||||
|
if %N_FAIL% gtr 0 set "EXITCODE=1"
|
||||||
|
if %N_WARN% gtr 0 if "%EXITCODE%"=="0" set "EXITCODE=2"
|
||||||
|
goto finish
|
||||||
|
|
||||||
|
:usage
|
||||||
|
call :show_usage
|
||||||
|
set "EXITCODE=0"
|
||||||
|
goto finish
|
||||||
|
|
||||||
|
rem ------------------------------ cleanup ------------------------------
|
||||||
|
:finish
|
||||||
|
if defined TMPLIST del /f /q "%TMPLIST%" >nul 2>&1
|
||||||
|
if "%PAUSE_AT_END%"=="1" pause
|
||||||
|
endlocal & exit /b %EXITCODE%
|
||||||
|
|
||||||
|
:err_nohome
|
||||||
|
echo [FAIL] environment variable USERPROFILE is not set, use /t to name a target
|
||||||
|
set "EXITCODE=2"
|
||||||
|
goto finish
|
||||||
|
|
||||||
|
rem ============================== subroutines ==============================
|
||||||
|
|
||||||
|
rem walk the skill directories of one category directory
|
||||||
|
:each_category
|
||||||
|
if "%CAT:~0,1%"=="." goto :eof
|
||||||
|
for /f "delims=" %%S in ('dir /b /ad /on "%ROOT%\%CAT%" 2^>nul') do (
|
||||||
|
set "SKILL=%%S"
|
||||||
|
call :each_skill
|
||||||
|
)
|
||||||
|
goto :eof
|
||||||
|
|
||||||
|
rem process one skill directory
|
||||||
|
:each_skill
|
||||||
|
if "%SKILL:~0,1%"=="." goto :eof
|
||||||
|
set "NAME=%SKILL%"
|
||||||
|
set "SRC=%ROOT%\%CAT%\%NAME%"
|
||||||
|
set "DEST=%TARGET%\%NAME%"
|
||||||
|
|
||||||
|
rem skill names must be unique; the first category scanned (alphabetical) wins
|
||||||
|
findstr /x /i /c:"\"%NAME%\"" "%TMPLIST%" >nul 2>&1
|
||||||
|
if not errorlevel 1 goto warn_dup
|
||||||
|
>>"%TMPLIST%" echo("%NAME%"
|
||||||
|
|
||||||
|
if not exist "%SRC%\SKILL.md" echo [NOTE] no SKILL.md inside "%SRC%", treated as a skill directory anyway
|
||||||
|
|
||||||
|
if /i "%MODE%"=="unlink" goto entry_unlink
|
||||||
|
if /i "%MODE%"=="copy" goto entry_copy
|
||||||
|
goto entry_link
|
||||||
|
|
||||||
|
rem ------------------------------ link mode ------------------------------
|
||||||
|
:entry_link
|
||||||
|
if not exist "%DEST%" goto link_new
|
||||||
|
if not exist "%DEST%\" goto warn_file
|
||||||
|
call :resolve_dest
|
||||||
|
if not defined CURTARGET goto warn_dir
|
||||||
|
call :same_source
|
||||||
|
if "%SAME%"=="1" goto skip_same
|
||||||
|
echo [REPL] "%DEST%" (was "%CURTARGET%") -^> "%SRC%"
|
||||||
|
set /a N_REPLACED+=1 >nul
|
||||||
|
set "REPLACING=1"
|
||||||
|
if "%DRYRUN%"=="1" goto :eof
|
||||||
|
call :remove_link
|
||||||
|
if "%OK%"=="0" goto fail_remove
|
||||||
|
:link_new
|
||||||
|
if "%DRYRUN%"=="1" goto link_done
|
||||||
|
call :make_link
|
||||||
|
if "%OK%"=="0" goto fail_link
|
||||||
|
:link_done
|
||||||
|
if defined REPLACING goto :eof
|
||||||
|
set /a N_LINKED+=1 >nul
|
||||||
|
echo [NEW] "%SRC%" -^> "%DEST%"
|
||||||
|
goto :eof
|
||||||
|
|
||||||
|
rem ------------------------------ copy mode ------------------------------
|
||||||
|
:entry_copy
|
||||||
|
if not exist "%DEST%" goto copy_do
|
||||||
|
if not exist "%DEST%\" goto warn_file
|
||||||
|
call :resolve_dest
|
||||||
|
if not defined CURTARGET goto copy_update
|
||||||
|
echo [REPL] "%DEST%" is a link, copying a real directory instead
|
||||||
|
set /a N_REPLACED+=1 >nul
|
||||||
|
if "%DRYRUN%"=="1" goto :eof
|
||||||
|
call :remove_link
|
||||||
|
if "%OK%"=="0" goto fail_remove
|
||||||
|
goto copy_do
|
||||||
|
:copy_update
|
||||||
|
echo [SYNC] updating the existing directory "%DEST%"
|
||||||
|
:copy_do
|
||||||
|
if "%DRYRUN%"=="1" goto copy_done
|
||||||
|
robocopy "%SRC%" "%DEST%" /E /NFL /NDL /NJH /NJS /NP /R:1 /W:1 >nul 2>&1
|
||||||
|
if errorlevel 8 goto fail_copy
|
||||||
|
:copy_done
|
||||||
|
set /a N_COPIED+=1 >nul
|
||||||
|
echo [COPY] "%SRC%" -^> "%DEST%"
|
||||||
|
goto :eof
|
||||||
|
|
||||||
|
rem ------------------------------ unlink mode ------------------------------
|
||||||
|
:entry_unlink
|
||||||
|
if not exist "%DEST%" goto :eof
|
||||||
|
if not exist "%DEST%\" goto skip_unlink_file
|
||||||
|
call :resolve_dest
|
||||||
|
if not defined CURTARGET goto skip_unlink_dir
|
||||||
|
call :in_repo
|
||||||
|
if "%INREPO%"=="0" goto skip_unlink_other
|
||||||
|
echo [DEL] "%DEST%" (was "%CURTARGET%")
|
||||||
|
set /a N_REMOVED+=1 >nul
|
||||||
|
if "%DRYRUN%"=="1" goto :eof
|
||||||
|
call :remove_link
|
||||||
|
if "%OK%"=="0" goto fail_remove
|
||||||
|
goto :eof
|
||||||
|
|
||||||
|
rem ------------------------------ helpers ------------------------------
|
||||||
|
|
||||||
|
rem read the link target of %DEST% (junctions and symlinks); stays empty for a real directory
|
||||||
|
:resolve_dest
|
||||||
|
set "CURTARGET="
|
||||||
|
if not exist "%TARGET%\" goto :eof
|
||||||
|
for /f "tokens=2 delims=[]" %%T in ('dir /a:l "%TARGET%" 2^>nul ^| findstr /i /c:" %NAME% ["') do set "CURTARGET=%%T"
|
||||||
|
goto :eof
|
||||||
|
|
||||||
|
rem does %CURTARGET% denote the same directory as %SRC%? compares long and 8.3
|
||||||
|
rem names, case-insensitively, so re-runs stay idempotent
|
||||||
|
:same_source
|
||||||
|
set "SAME=0"
|
||||||
|
set "A=%CURTARGET%"
|
||||||
|
set "B=%SRC%"
|
||||||
|
if "%A%"=="" goto :eof
|
||||||
|
if "%A:~0,4%"=="\??\" set "A=%A:~4%"
|
||||||
|
if /i "%A%"=="%B%" set "SAME=1"
|
||||||
|
for %%X in ("%A%") do set "A2=%%~fsX"
|
||||||
|
for %%Y in ("%B%") do set "B2=%%~fsY"
|
||||||
|
if /i "%A2%"=="%B2%" set "SAME=1"
|
||||||
|
goto :eof
|
||||||
|
|
||||||
|
rem is the link target inside this repository?
|
||||||
|
:in_repo
|
||||||
|
set "INREPO=0"
|
||||||
|
set "P=%ROOT%\"
|
||||||
|
call set "REST=%%CURTARGET:%P%=%%"
|
||||||
|
if not "%REST%"=="%CURTARGET%" set "INREPO=1"
|
||||||
|
goto :eof
|
||||||
|
|
||||||
|
rem create the link: junction first, symlink as fallback, clearing stale entries
|
||||||
|
:make_link
|
||||||
|
set "OK=0"
|
||||||
|
call :mk_verify J
|
||||||
|
if "%OK%"=="1" goto :eof
|
||||||
|
call :remove_link
|
||||||
|
set "OK=0"
|
||||||
|
call :mk_verify J
|
||||||
|
if "%OK%"=="1" goto :eof
|
||||||
|
call :mk_verify D
|
||||||
|
goto :eof
|
||||||
|
|
||||||
|
rem run one mklink and verify that the link really resolves to %SRC%
|
||||||
|
:mk_verify
|
||||||
|
mklink /%1 "%DEST%" "%SRC%" >nul 2>&1
|
||||||
|
call :resolve_dest
|
||||||
|
if not defined CURTARGET goto :eof
|
||||||
|
call :same_source
|
||||||
|
if "%SAME%"=="1" set "OK=1"
|
||||||
|
goto :eof
|
||||||
|
|
||||||
|
rem delete the link itself: rd without /s never recurses into the link target
|
||||||
|
:remove_link
|
||||||
|
set "OK=0"
|
||||||
|
rd "%DEST%" >nul 2>&1
|
||||||
|
if exist "%DEST%\" goto :eof
|
||||||
|
if exist "%DEST%" del /f /q "%DEST%" >nul 2>&1
|
||||||
|
if exist "%DEST%" goto :eof
|
||||||
|
set "OK=1"
|
||||||
|
goto :eof
|
||||||
|
|
||||||
|
rem ------------------------------ notices ------------------------------
|
||||||
|
:warn_dup
|
||||||
|
echo [WARN] duplicate skill name, skipped "%ROOT%\%CAT%\%NAME%" (the first category scanned wins)
|
||||||
|
set /a N_WARN+=1 >nul
|
||||||
|
goto :eof
|
||||||
|
|
||||||
|
:warn_file
|
||||||
|
echo [WARN] a plain file already exists at "%DEST%", skipped
|
||||||
|
set /a N_WARN+=1 >nul
|
||||||
|
goto :eof
|
||||||
|
|
||||||
|
:warn_dir
|
||||||
|
echo [WARN] a real directory (not a link) already exists at "%DEST%", skipped - resolve it manually
|
||||||
|
set /a N_WARN+=1 >nul
|
||||||
|
goto :eof
|
||||||
|
|
||||||
|
:skip_same
|
||||||
|
set /a N_SKIPPED+=1 >nul
|
||||||
|
echo [SKIP] already linked: "%DEST%"
|
||||||
|
goto :eof
|
||||||
|
|
||||||
|
:skip_unlink_file
|
||||||
|
echo [SKIP] plain file, left untouched: "%DEST%"
|
||||||
|
set /a N_SKIPPED+=1 >nul
|
||||||
|
goto :eof
|
||||||
|
|
||||||
|
:skip_unlink_dir
|
||||||
|
echo [SKIP] real directory, left untouched: "%DEST%"
|
||||||
|
set /a N_SKIPPED+=1 >nul
|
||||||
|
goto :eof
|
||||||
|
|
||||||
|
:skip_unlink_other
|
||||||
|
echo [SKIP] link points outside this repository, left untouched: "%DEST%" -^> "%CURTARGET%"
|
||||||
|
set /a N_SKIPPED+=1 >nul
|
||||||
|
goto :eof
|
||||||
|
|
||||||
|
:fail_target
|
||||||
|
echo [FAIL] cannot create target directory "%TARGET%"
|
||||||
|
set /a N_FAIL+=1 >nul
|
||||||
|
goto summary
|
||||||
|
|
||||||
|
:fail_remove
|
||||||
|
echo [FAIL] cannot remove the existing entry "%DEST%"
|
||||||
|
set /a N_FAIL+=1 >nul
|
||||||
|
goto :eof
|
||||||
|
|
||||||
|
:fail_link
|
||||||
|
echo [FAIL] cannot create the link "%DEST%"
|
||||||
|
echo try /c (copy mode); network paths only work with copy mode.
|
||||||
|
set /a N_FAIL+=1 >nul
|
||||||
|
goto :eof
|
||||||
|
|
||||||
|
:fail_copy
|
||||||
|
echo [FAIL] copy failed: "%DEST%"
|
||||||
|
set /a N_FAIL+=1 >nul
|
||||||
|
goto :eof
|
||||||
|
|
||||||
|
rem ------------------------------ help ------------------------------
|
||||||
|
:show_usage
|
||||||
|
echo(
|
||||||
|
echo AgentSkills skills linker for Windows - the counterpart of skills_linker.sh
|
||||||
|
echo(
|
||||||
|
echo usage: %~nx0 [options]
|
||||||
|
echo(
|
||||||
|
echo /l /link link mode (default): junction, symlink as fallback
|
||||||
|
echo /c /copy copy mode: place real copies (use when links are unusable)
|
||||||
|
echo /u /unlink unlink mode: remove only links pointing into this repo
|
||||||
|
echo /t /target DIR target directory (default %%USERPROFILE%%\.agents\skills)
|
||||||
|
echo /n /dry-run dry run: print the plan, write and delete nothing
|
||||||
|
echo /? /help show this help
|
||||||
|
echo(
|
||||||
|
echo notes:
|
||||||
|
echo * repo root = the directory holding this script; it scans the two levels
|
||||||
|
echo "category\skill" below it. Directories starting with a dot are skipped.
|
||||||
|
echo * already linked to the same skill -^> skip; linked elsewhere -^> replace;
|
||||||
|
echo a real directory with the same name -^> skip and warn (never overwritten).
|
||||||
|
echo * duplicate skill names keep the first category scanned and count as warnings.
|
||||||
|
echo * a double click waits for a key at the end; calls with arguments or
|
||||||
|
echo redirected input never wait, so the script stays automation friendly.
|
||||||
|
echo * exit codes: 0 all good, 1 failures, 2 warnings (name conflict or duplicate).
|
||||||
|
goto :eof
|
||||||
Executable → Regular
File mode changed.
Reference in new issue
Block a user