✨ feat(skills_linker): 添加一键安装脚本。
This commit is contained in:
1 parent
2ce3f0173d
commit
707a6f1ef1
3 files changed
+445
-3
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user