返回 Skills 目錄
dontbesilent2025/dbskill已通過檢查

SKILL DETAIL

dbs-install-skill

dontbesilent2025/dbskill/dbs-install-skill

dbs-install-skill is a tool for installing Skills into various agent environments. It supports generic Agents via the common entry point ~/.agents/skills, as well as clients requiring dedicated directories such as Claude Code, WorkBuddy, Hermes Agent, Kiro, Qwen Code, and Cline, and generates a thin adapter layer for Grok. The script automatically routes without requiring user-specified modes and cleans up historical redundant symlinks to ensure each Skill appears only once in common-compatible clients. The tool provides three core commands: link, status, and unlink, for installation, status checking, and uninstallation respectively. During installation, the script prioritizes the name field from SKILL.md frontmatter as the entry name and adheres to principles such as not overwriting real directories and never deleting source Skills. Uninstallation removes only derived artifacts, leaving the source Skill intact.

安裝量 · 277查看來源

Installation

npx skills add https://github.com/dontbesilent2025/dbskill --skill dbs-install-skill

技能檔案

SKILL.md

最近同步 · 2026年10月18日

agents/openai.yaml›
interface:
  display_name: "dbs-install-skill(Skill 安装管理)"
  short_description: "把 Skill 安装到 Claude、Codex、WorkBuddy 与 Grok"
  default_prompt: "使用 $dbs-install-skill,把目标 Skill 安装到本机已有的 Agent。"
scripts/install-skill.sh›
#!/usr/bin/env bash
set -euo pipefail

usage() {
  cat <<'USAGE'
Usage:
  install-skill.sh link <skill-name-or-path>
  install-skill.sh unlink <skill-name-or-path>
  install-skill.sh status <skill-name-or-path>

Examples:
  install-skill.sh link dbs-diagnosis
  install-skill.sh link skills/dbs-diagnosis
  install-skill.sh link skills
  install-skill.sh status /absolute/path/to/skill

Routing:
  ~/.agents/skills is the shared skill bus. Codex, GitHub Copilot, Gemini CLI,
  Cursor, Augment, Roo Code, OpenCode, and OpenHands read skills from there.

  Claude Code, WorkBuddy, Hermes Agent, Kiro, Qwen Code, and Cline receive
  native links only when their home directories already exist. Windows uses
  directory Junctions; Unix-like systems use symbolic links.

  Grok receives a thin bridge instead of a filesystem link. Legacy and
  duplicate links under shared-compatible or retired host directories are
  removed automatically when they point to the selected source.
USAGE
}

die() {
  echo "✗ $*" >&2
  exit 1
}

repo_root() {
  local script_dir
  script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  cd "$script_dir/../../.." && pwd
}

script_dir() {
  cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P
}

is_windows_layer() {
  case "${DBS_INSTALL_PLATFORM:-}" in
    windows) return 0 ;;
    unix) return 1 ;;
  esac

  case "$(uname -s)" in
    MINGW*|MSYS*|CYGWIN*) return 0 ;;
    *) return 1 ;;
  esac
}

windows_powershell() {
  local candidate

  if [[ -n "${DBS_INSTALL_POWERSHELL:-}" ]]; then
    printf '%s\n' "$DBS_INSTALL_POWERSHELL"
    return 0
  fi

  for candidate in pwsh.exe powershell.exe pwsh powershell; do
    if command -v "$candidate" >/dev/null 2>&1; then
      command -v "$candidate"
      return 0
    fi
  done

  return 1
}

windows_path() {
  local path="$1"
  local converter="${DBS_INSTALL_CYGPATH:-cygpath}"

  command -v "$converter" >/dev/null 2>&1 || die "Windows 环境缺少 cygpath,无法安全创建 Junction"
  "$converter" -aw "$path"
}

windows_long_path() {
  local path="$1"
  local converter="${DBS_INSTALL_CYGPATH:-cygpath}"

  command -v "$converter" >/dev/null 2>&1 || die "Windows 环境缺少 cygpath,无法比较路径"
  "$converter" -awl "$path" 2>/dev/null || "$converter" -aw "$path"
}

posix_path() {
  local path="$1"
  local converter="${DBS_INSTALL_CYGPATH:-cygpath}"

  command -v "$converter" >/dev/null 2>&1 || return 1
  "$converter" -au "$path"
}

run_windows_junction() {
  local action="$1"
  local path="$2"
  local target="${3:-}"
  local powershell
  local helper
  local path_windows
  local target_windows=""
  local -a powershell_args

  powershell="$(windows_powershell)" || die "Windows 环境缺少 PowerShell,无法安全管理 Junction"
  helper="$(windows_path "$(script_dir)/windows-junction.ps1")"
  path_windows="$(windows_path "$path")"
  if [[ -n "$target" ]]; then
    target_windows="$(windows_path "$target")"
  fi

  powershell_args=(
    -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass
    -File "$helper" -Action "$action" -Path "$path_windows"
  )
  if [[ -n "$target_windows" ]]; then
    powershell_args+=(-Target "$target_windows")
  fi

  MSYS2_ARG_CONV_EXCL='*' "$powershell" "${powershell_args[@]}"
}

resolve_candidate() {
  local input="$1"
  local root="$2"
  local candidate

  if [[ "$input" = /* ]]; then
    candidate="$input"
  elif [[ -d "$PWD/$input" ]]; then
    candidate="$PWD/$input"
  elif [[ -d "$root/$input" ]]; then
    candidate="$root/$input"
  elif [[ -d "$root/skills/$input" ]]; then
    candidate="$root/skills/$input"
  else
    die "找不到 Skill 或 Skill 集合目录:$input"
  fi

  candidate="$(cd "$candidate" && pwd -P)"
  printf '%s\n' "$candidate"
}

list_skill_sources() {
  local candidate="$1"
  local found=0
  local skill_dir

  if [[ -f "$candidate/SKILL.md" ]]; then
    printf '%s\n' "$candidate"
    return 0
  fi

  for skill_dir in "$candidate"/*; do
    [[ "$(basename "$skill_dir")" == "private" ]] && continue
    [[ -f "$skill_dir/SKILL.md" ]] || continue
    found=1
    (cd "$skill_dir" && pwd -P)
  done

  [[ "$found" -eq 1 ]] || die "$candidate 里没有 SKILL.md,也没有包含 SKILL.md 的一级子目录"
}

skill_name() {
  local src="$1"
  local skill_file="$src/SKILL.md"
  local name

  name="$(awk '
    NR == 1 && $0 == "---" { in_frontmatter = 1; next }
    in_frontmatter && $0 == "---" { exit }
    in_frontmatter && /^name:[[:space:]]*/ {
      sub(/^name:[[:space:]]*/, "")
      gsub(/^[[:space:]]+|[[:space:]]+$/, "")
      print
      exit
    }
  ' "$skill_file")"

  if [[ "$name" == \"*\" && "$name" == *\" ]] || [[ "$name" == \'*\' && "$name" == *\' ]]; then
    name="${name:1:${#name}-2}"
  fi
  [[ -n "$name" ]] || name="$(basename "$src")"

  case "$name" in
    .|..|*/*) die "Skill name 不合法:$name($skill_file)" ;;
  esac
  printf '%s\n' "$name"
}

interface_gate_required() {
  local current="$1"
  local parent

  while true; do
    if [[ -f "$current/AGENTS.md" ]] && grep -Fq '<!-- skill-interface-gate: required -->' "$current/AGENTS.md"; then
      return 0
    fi
    parent="$(dirname "$current")"
    [[ "$parent" != "$current" ]] || return 1
    current="$parent"
  done
}

validate_interface_gate() {
  local candidate="$1"
  local validator

  interface_gate_required "$candidate" || return 0
  validator="$(repo_root)/tools/check-skill-interface-metadata.py"
  [[ -f "$validator" ]] || die "已启用 Skill 界面元数据闸门,但找不到校验器:$validator"
  echo "== Skill 界面元数据闸门 =="
  python3 "$validator" "$candidate" || die "Skill 界面元数据校验失败,已停止安装"
}

INSTALL_HOME="${DBS_INSTALL_HOME:-$HOME}"
SHARED_TARGET_DIR="$INSTALL_HOME/.agents/skills"

# 这些客户端没有采用 ~/.agents/skills,或当前仍需要原生目录。
NATIVE_TARGET_DIRS=(
  "$INSTALL_HOME/.claude/skills"
  "$INSTALL_HOME/.workbuddy/skills"
  "$INSTALL_HOME/.hermes/skills"
  "$INSTALL_HOME/.kiro/skills"
  "$INSTALL_HOME/.qwen/skills"
  "$INSTALL_HOME/.cline/skills"
)

# 前 8 项已能读取 ~/.agents/skills;其余是旧版脚本曾写入、现已停止维护的目录。
# link 会清理其中指向当前源 Skill 的链接,避免同一 Skill 多入口和随机扩散。
REDUNDANT_TARGET_DIRS=(
  "$INSTALL_HOME/.codex/skills"
  "$INSTALL_HOME/.copilot/skills"
  "$INSTALL_HOME/.gemini/skills"
  "$INSTALL_HOME/.cursor/skills"
  "$INSTALL_HOME/.augment/skills"
  "$INSTALL_HOME/.roo/skills"
  "$INSTALL_HOME/.config/opencode/skills"
  "$INSTALL_HOME/.openhands/skills"
  "$INSTALL_HOME/.kilocode/skills"
  "$INSTALL_HOME/.trae/skills"
  "$INSTALL_HOME/.trae-cn/skills"
  "$INSTALL_HOME/.codebuddy/skills"
  "$INSTALL_HOME/.zencoder/skills"
  "$INSTALL_HOME/.continue/skills"
  "$INSTALL_HOME/.aider-desk/skills"
  "$INSTALL_HOME/.factory/skills"
  "$INSTALL_HOME/.forge/skills"
  "$INSTALL_HOME/.vibe/skills"
  "$INSTALL_HOME/.codestudio/skills"
  "$INSTALL_HOME/.codemaker/skills"
  "$INSTALL_HOME/.codeartsdoer/skills"
  "$INSTALL_HOME/.junie/skills"
  "$INSTALL_HOME/.qoder/skills"
  "$INSTALL_HOME/.openclaw/skills"
)

host_root_for() {
  dirname "$1"
}

path_entry_exists() {
  local path="$1"
  local parent
  local entry

  [[ -e "$path" || -L "$path" ]] && return 0
  parent="$(dirname "$path")"
  [[ -d "$parent" ]] || return 1
  while IFS= read -r entry; do
    [[ "$entry" == "$path" ]] && return 0
  done < <(find "$parent" -mindepth 1 -maxdepth 1 -print)
  return 1
}

normalized_path_for_compare() {
  local path="$1"
  local normalized

  if is_windows_layer; then
    normalized="$(windows_long_path "$path")"
    normalized="${normalized//$'\r'/}"
    normalized="$(printf '%s' "$normalized" | tr '\\\\' '/' | tr '[:upper:]' '[:lower:]')"
    printf '%s\n' "${normalized%/}"
  else
    printf '%s\n' "${path%/}"
  fi
}

paths_are_same() {
  local left
  local right

  left="$(normalized_path_for_compare "$1")"
  right="$(normalized_path_for_compare "$2")"
  [[ "$left" == "$right" ]] && return 0
  is_windows_layer || return 1
  run_windows_junction same "$1" "$2" >/dev/null 2>&1
}

path_is_under() {
  local path
  local root

  path="$(normalized_path_for_compare "$1")"
  root="$(normalized_path_for_compare "$2")"
  case "$path" in
    "$root"|"$root"/*) return 0 ;;
    *) ;;
  esac

  is_windows_layer || return 1
  run_windows_junction under "$1" "$2" >/dev/null 2>&1
}

is_managed_link() {
  local link="$1"

  [[ -L "$link" ]] && return 0
  is_windows_layer || return 1
  path_entry_exists "$link" || return 1
  run_windows_junction test "$link" >/dev/null 2>&1
}

raw_link_target() {
  local link="$1"
  local windows_target

  if [[ -L "$link" ]]; then
    readlink "$link"
    return
  fi

  is_windows_layer || return 1
  path_entry_exists "$link" || return 1
  windows_target="$(run_windows_junction target "$link")" || return 1
  windows_target="${windows_target//$'\r'/}"
  posix_path "$windows_target"
}

remove_managed_link() {
  local link="$1"

  if [[ -L "$link" ]]; then
    rm "$link"
    return
  fi

  is_windows_layer || return 1
  run_windows_junction remove "$link" >/dev/null
}

create_managed_link() {
  local src="$1"
  local link="$2"

  if is_windows_layer; then
    run_windows_junction create "$link" "$src" >/dev/null
  else
    ln -s "$src" "$link"
  fi
}

managed_links_in_dir() {
  local dest_dir="$1"
  local entry
  local windows_entry

  [[ -d "$dest_dir" ]] || return 0
  if is_windows_layer; then
    while IFS= read -r windows_entry; do
      windows_entry="${windows_entry//$'\r'/}"
      [[ -n "$windows_entry" ]] || continue
      posix_path "$windows_entry"
    done < <(run_windows_junction list "$dest_dir")
    return 0
  fi

  while IFS= read -r entry; do
    is_managed_link "$entry" && printf '%s\n' "$entry"
  done < <(find "$dest_dir" -mindepth 1 -maxdepth 1 -print | sort)
}

resolved_link_target() {
  local link="$1"
  local target

  target="$(raw_link_target "$link")" || return 1
  resolved_target_from_raw "$link" "$target"
}

resolved_target_from_raw() {
  local link="$1"
  local target="$2"
  local parent

  if [[ "$target" = /* ]]; then
    [[ -d "$target" ]] || return 1
    (cd "$target" && pwd -P)
    return
  fi

  parent="$(dirname "$link")"
  [[ -d "$parent/$target" ]] || return 1
  (cd "$parent/$target" && pwd -P)
}

link_points_to() {
  local link="$1"
  local src="$2"
  local raw_target

  raw_target="$(raw_link_target "$link" 2>/dev/null)" || return 1
  target_points_to "$link" "$raw_target" "$src"
}

target_points_to() {
  local link="$1"
  local raw_target="$2"
  local src="$3"
  local resolved_target

  paths_are_same "$raw_target" "$src" && return 0

  resolved_target="$(resolved_target_from_raw "$link" "$raw_target" 2>/dev/null || true)"
  [[ -n "$resolved_target" ]] && paths_are_same "$resolved_target" "$src"
}

link_targets_under() {
  local link="$1"
  local candidate="$2"
  local raw_target
  local resolved_target

  raw_target="$(raw_link_target "$link" 2>/dev/null)" || return 1
  path_is_under "$raw_target" "$candidate" && return 0

  resolved_target="$(resolved_target_from_raw "$link" "$raw_target" 2>/dev/null || true)"
  [[ -n "$resolved_target" ]] && path_is_under "$resolved_target" "$candidate" && return 0
  return 1
}

link_one() {
  local src="$1"
  local dest_dir="$2"
  local name="$3"
  local create_parent="$4"
  local link="$dest_dir/$name"
  local host_root
  local raw_target

  host_root="$(host_root_for "$dest_dir")"
  if [[ "$create_parent" -eq 0 && ! -d "$host_root" ]]; then
    echo "· $host_root 不存在,跳过"
    return 0
  fi

  mkdir -p "$dest_dir"

  raw_target="$(raw_link_target "$link" 2>/dev/null || true)"
  if [[ -n "$raw_target" ]]; then
    if target_points_to "$link" "$raw_target" "$src"; then
      echo "✓ $link -> $raw_target"
      return 0
    fi
    echo "✗ $link 指向其他源 $raw_target,已保留"
    return 2
  fi

  if [[ -e "$link" ]]; then
    echo "✗ $link 是真实目录或文件,已保留"
    if is_windows_layer && [[ -d "$link" ]]; then
      echo "  可能是旧版 MSYS 生成的实体副本;请核对并手动移走后重试"
    fi
    return 2
  fi

  create_managed_link "$src" "$link"
  echo "✓ $link -> $src"
}

unlink_if_points_to() {
  local src="$1"
  local dest_dir="$2"
  local name="$3"
  local link="$dest_dir/$name"
  local raw_target

  raw_target="$(raw_link_target "$link" 2>/dev/null || true)"
  if [[ -n "$raw_target" ]]; then
    if target_points_to "$link" "$raw_target" "$src"; then
      remove_managed_link "$link"
      echo "✓ 已移除链接 $link"
    else
      echo "✗ $link 指向其他源,已保留"
      return 2
    fi
  elif [[ -e "$link" ]]; then
    echo "✗ $link 是真实目录或文件,已保留"
    return 2
  else
    echo "· $link 不存在,跳过"
  fi
}

remove_redundant_one() {
  local src="$1"
  local dest_dir="$2"
  local name="$3"
  local link="$dest_dir/$name"
  local raw_target

  [[ -e "$dest_dir" || -L "$dest_dir" ]] || return 0

  raw_target="$(raw_link_target "$link" 2>/dev/null || true)"
  if [[ -n "$raw_target" ]]; then
    if target_points_to "$link" "$raw_target" "$src"; then
      remove_managed_link "$link"
      echo "✓ 已清理冗余链接 $link"
    else
      echo "✗ $link 指向其他源,已保留"
      return 2
    fi
  elif [[ -e "$link" ]]; then
    echo "✗ $link 是真实目录或文件,已保留"
    return 2
  fi
}

remove_redundant_collection_links() {
  local candidate="$1"
  local dest_dir
  local link

  for dest_dir in "${REDUNDANT_TARGET_DIRS[@]}"; do
    [[ -d "$dest_dir" ]] || continue
    while IFS= read -r link; do
      if link_targets_under "$link" "$candidate"; then
        remove_managed_link "$link"
        echo "✓ 已清理冗余链接 $link"
      fi
    done < <(managed_links_in_dir "$dest_dir")
  done
}

remove_duplicate_aliases() {
  local candidate="$1"
  local dest_dir
  local link
  local target
  local canonical
  local canonical_name

  for dest_dir in "$SHARED_TARGET_DIR" "${NATIVE_TARGET_DIRS[@]}"; do
    [[ -d "$dest_dir" ]] || continue
    while IFS= read -r link; do
      link_targets_under "$link" "$candidate" || continue
      target="$(resolved_link_target "$link" 2>/dev/null || true)"
      [[ -n "$target" ]] || continue
      [[ -f "$target/SKILL.md" ]] || continue
      canonical_name="$(skill_name "$target")"
      canonical="$dest_dir/$canonical_name"
      if ! paths_are_same "$link" "$canonical" && link_points_to "$canonical" "$target"; then
        remove_managed_link "$link"
        echo "✓ 已清理重复别名 $link"
      fi
    done < <(managed_links_in_dir "$dest_dir")
  done
}

remove_stale_collection_artifacts() {
  local candidate="$1"
  local dest_dir
  local link
  local raw_target
  local grok_skill
  local source_file
  local grok_dir

  for dest_dir in "$SHARED_TARGET_DIR" "${NATIVE_TARGET_DIRS[@]}"; do
    [[ -d "$dest_dir" ]] || continue
    while IFS= read -r link; do
      raw_target="$(raw_link_target "$link")"
      if path_is_under "$raw_target" "$candidate" && [[ ! -f "$raw_target/SKILL.md" ]]; then
        remove_managed_link "$link"
        echo "✓ 已清理失效链接 $link"
      fi
    done < <(managed_links_in_dir "$dest_dir")
  done

  [[ -d "$INSTALL_HOME/.grok/skills" ]] || return 0
  while IFS= read -r grok_skill; do
    grep -q '^## Grok Bridge$' "$grok_skill" || continue
    source_file="$(grep -m 1 '^- Source of truth:' "$grok_skill" | sed 's/^- Source of truth: //')"
    case "$source_file" in
      "$candidate"/SKILL.md|"$candidate"/*/SKILL.md)
        if [[ ! -f "$source_file" ]]; then
          grok_dir="$(dirname "$grok_skill")"
          rm -rf "$grok_dir"
          echo "✓ 已清理失效 Grok bridge $grok_dir"
        fi
        ;;
    esac
  done < <(find "$INSTALL_HOME/.grok/skills" -mindepth 2 -maxdepth 2 -name SKILL.md -type f | sort)
}

status_one() {
  local src="$1"
  local dest_dir="$2"
  local name="$3"
  local label="$4"
  local link="$dest_dir/$name"
  local raw_target

  raw_target="$(raw_link_target "$link" 2>/dev/null || true)"
  if [[ -n "$raw_target" ]]; then
    if target_points_to "$link" "$raw_target" "$src"; then
      echo "✓ ${label}:$link -> $raw_target"
    else
      echo "✗ ${label}:$link 指向其他源 $raw_target"
      return 2
    fi
  elif [[ -e "$link" ]]; then
    echo "✗ ${label}:$link 存在,但不是受管链接"
    return 2
  else
    echo "· ${label}:$link 未桥接"
  fi
}

status_redundant_one() {
  local src="$1"
  local dest_dir="$2"
  local name="$3"
  local link="$dest_dir/$name"
  local raw_target

  raw_target="$(raw_link_target "$link" 2>/dev/null || true)"
  if [[ -n "$raw_target" ]]; then
    if target_points_to "$link" "$raw_target" "$src"; then
      echo "✗ 发现冗余入口:$link -> $raw_target"
    else
      echo "✗ 公共兼容客户端存在同名其他来源:$link -> $raw_target"
    fi
    return 2
  fi

  if [[ -e "$link" ]]; then
    echo "✗ 公共兼容客户端存在同名真实目录或文件:$link"
    return 2
  fi
  return 0
}

link_grok_one() {
  local src="$1"
  local name="$2"
  local grok_home="$INSTALL_HOME/.grok"
  local dir="$grok_home/skills/$name"
  local skill_file="$dir/SKILL.md"
  local existing_source

  if [[ ! -d "$grok_home" ]]; then
    echo "· $grok_home 不存在,跳过"
    return 0
  fi

  if is_managed_link "$dir"; then
    remove_managed_link "$dir"
  elif [[ -e "$dir" && ! -d "$dir" ]]; then
    echo "✗ $dir 是真实文件,已保留"
    return 2
  elif [[ -d "$dir" && -f "$skill_file" ]] && ! grep -q '^## Grok Bridge$' "$skill_file"; then
    echo "✗ $dir 是真实 Grok Skill,已保留"
    return 2
  elif [[ -d "$dir" && ! -f "$skill_file" ]]; then
    echo "✗ $dir 是真实目录,已保留"
    return 2
  elif [[ -f "$skill_file" ]]; then
    existing_source="$(grok_bridge_source "$skill_file")"
    if [[ "$existing_source" != "$src/SKILL.md" && -f "$existing_source" ]]; then
      echo "✗ $dir 是指向其他真源的 Grok 适配层($existing_source),已保留"
      return 2
    fi
  fi

  mkdir -p "$dir"
  cat > "$skill_file" <<EOF
---
name: $name
user_invocable: true
description: |
  $name bridge。在 Grok TUI 中可通过 /$name 触发;触发后必须先读取项目真源 SKILL.md。
---
# $name

## Grok Bridge

- Source of truth: $src/SKILL.md
- Read the source-of-truth file before executing this skill.
- Follow the source file's workflow, constraints, examples, and output format.
- Treat this file as a thin Grok bridge only; do not maintain long-form logic here.

## 使用说明

1. 在 Grok TUI 中输入 \`/$name\` 即可触发。
2. Grok 会优先使用本 bridge 指向的真源。
3. 如需更新,直接修改真源。
EOF
  echo "✓ $dir -> $src"
}

grok_bridge_source() {
  grep -m 1 '^- Source of truth:' "$1" | sed 's/^- Source of truth: //'
}

unlink_grok_one() {
  local src="$1"
  local name="$2"
  local grok_home="$INSTALL_HOME/.grok"
  local dir="$grok_home/skills/$name"
  local skill_file="$dir/SKILL.md"

  if [[ ! -d "$grok_home" ]]; then
    echo "· $grok_home 不存在,跳过"
    return 0
  fi

  if is_managed_link "$dir"; then
    remove_managed_link "$dir"
    echo "✓ 已移除链接 $dir"
  elif [[ -d "$dir" && -f "$skill_file" ]] && grep -q '^## Grok Bridge$' "$skill_file"; then
    if [[ "$(grok_bridge_source "$skill_file")" != "$src/SKILL.md" ]]; then
      echo "✗ $dir 指向其他真源($(grok_bridge_source "$skill_file")),已保留"
      return 2
    fi
    rm -rf "$dir"
    echo "✓ 已移除 Grok bridge $dir"
  elif [[ -e "$dir" ]]; then
    echo "✗ $dir 是真实目录或文件,已保留"
    return 2
  else
    echo "· $dir 不存在,跳过"
  fi
}

status_grok_one() {
  local name="$1"
  local grok_home="$INSTALL_HOME/.grok"
  local dir="$grok_home/skills/$name"
  local skill_file="$dir/SKILL.md"
  local source

  if [[ ! -d "$grok_home" ]]; then
    echo "· Grok:$grok_home 不存在"
    return 0
  fi

  if [[ -d "$dir" && -f "$skill_file" ]] && grep -q '^## Grok Bridge$' "$skill_file"; then
    source="$(grep -m 1 '^- Source of truth:' "$skill_file" | sed 's/^- Source of truth: //')"
    if grep -q '^user_invocable: true$' "$skill_file"; then
      echo "✓ Grok:$dir -> $source"
    else
      echo "✗ Grok:$dir 缺少 user_invocable: true"
      return 2
    fi
  elif [[ -e "$dir" ]]; then
    echo "✗ Grok:$dir 存在,但并非 dbs-install-skill 生成的适配层"
    return 2
  else
    echo "· Grok:$dir 未桥接"
  fi
}

main() {
  if [[ $# -ne 2 ]]; then
    usage
    exit 1
  fi

  local action="$1"
  local input="$2"
  local root
  local candidate
  local src
  local name
  local target_dir
  local failed=0

  case "$action" in
    link|unlink|status) ;;
    *) usage; exit 1 ;;
  esac

  root="$(repo_root)"
  candidate="$(resolve_candidate "$input" "$root")"

  if [[ "$action" == "link" || "$action" == "status" ]]; then
    validate_interface_gate "$candidate"
  fi

  while IFS= read -r src; do
    name="$(skill_name "$src")"
    echo "== $name =="

    case "$action" in
      link)
        link_one "$src" "$SHARED_TARGET_DIR" "$name" 1 || failed=1
        for target_dir in "${NATIVE_TARGET_DIRS[@]}"; do
          link_one "$src" "$target_dir" "$name" 0 || failed=1
        done
        for target_dir in "${REDUNDANT_TARGET_DIRS[@]}"; do
          remove_redundant_one "$src" "$target_dir" "$name" || failed=1
        done
        link_grok_one "$src" "$name" || failed=1
        ;;
      unlink)
        unlink_if_points_to "$src" "$SHARED_TARGET_DIR" "$name" || failed=1
        for target_dir in "${NATIVE_TARGET_DIRS[@]}" "${REDUNDANT_TARGET_DIRS[@]}"; do
          unlink_if_points_to "$src" "$target_dir" "$name" || failed=1
        done
        unlink_grok_one "$src" "$name" || failed=1
        ;;
      status)
        status_one "$src" "$SHARED_TARGET_DIR" "$name" "公共入口" || failed=1
        for target_dir in "${NATIVE_TARGET_DIRS[@]}"; do
          if [[ -d "$(host_root_for "$target_dir")" ]]; then
            status_one "$src" "$target_dir" "$name" "专属入口" || failed=1
          fi
        done
        for target_dir in "${REDUNDANT_TARGET_DIRS[@]}"; do
          status_redundant_one "$src" "$target_dir" "$name" || failed=1
        done
        status_grok_one "$name" || failed=1
        ;;
    esac
  done < <(list_skill_sources "$candidate")

  if [[ "$action" == "link" ]]; then
    remove_redundant_collection_links "$candidate"
    remove_duplicate_aliases "$candidate"
    remove_stale_collection_artifacts "$candidate"
  fi

  if [[ "$action" == "status" && "$failed" -eq 0 ]]; then
    echo "✓ 未发现冗余入口"
  fi

  exit "$failed"
}

main "$@"
scripts/windows-junction.ps1›
param(
  [Parameter(Mandatory = $true)]
  [ValidateSet("create", "target", "remove", "test", "list", "same", "under")]
  [string]$Action,

  [Parameter(Mandatory = $true)]
  [string]$Path,

  [string]$Target
)

$ErrorActionPreference = "Stop"

function Get-ManagedLink([string]$LiteralPath) {
  $item = Get-Item -LiteralPath $LiteralPath -Force -ErrorAction SilentlyContinue
  if ($null -eq $item) {
    return $null
  }

  if ($item.LinkType -in @("Junction", "SymbolicLink")) {
    return $item
  }

  $isReparsePoint = ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0
  if ($isReparsePoint -and $item.PSIsContainer) {
    return $item
  }

  return $null
}

function Get-ManagedLinkTarget($Item) {
  $linkTarget = @($Item.Target)[0]
  if (-not [string]::IsNullOrWhiteSpace($linkTarget)) {
    if (-not [System.IO.Path]::IsPathRooted($linkTarget)) {
      $linkTarget = Join-Path $Item.Parent.FullName $linkTarget
    }
    return [System.IO.Path]::GetFullPath($linkTarget)
  }

  $resolveMethod = $Item.GetType().GetMethod("ResolveLinkTarget", [type[]]@([bool]))
  if ($null -ne $resolveMethod) {
    $resolved = $Item.ResolveLinkTarget($false)
    if ($null -ne $resolved) {
      return $resolved.FullName
    }
  }

  return $null
}

function Get-ComparableFullName([string]$LiteralPath) {
  $item = Get-Item -LiteralPath $LiteralPath -Force -ErrorAction SilentlyContinue
  if ($null -eq $item) {
    return $null
  }

  return $item.FullName.TrimEnd(
    [System.IO.Path]::DirectorySeparatorChar,
    [System.IO.Path]::AltDirectorySeparatorChar
  )
}

switch ($Action) {
  "create" {
    if ([string]::IsNullOrWhiteSpace($Target)) {
      throw "create 操作缺少 Target"
    }

    if (Test-Path -LiteralPath $Path -PathType Any) {
      throw "目标已经存在:$Path"
    }

    New-Item -ItemType Junction -Path $Path -Target $Target | Out-Null
    $item = Get-ManagedLink $Path
    if ($null -eq $item) {
      throw "Junction 创建后校验失败:$Path"
    }
  }

  "target" {
    $item = Get-ManagedLink $Path
    if ($null -eq $item) {
      exit 1
    }

    $linkTarget = Get-ManagedLinkTarget $item
    if ([string]::IsNullOrWhiteSpace($linkTarget)) {
      exit 1
    }

    $linkTarget
  }

  "remove" {
    $item = Get-ManagedLink $Path
    if ($null -eq $item) {
      throw "拒绝删除非 Junction/SymbolicLink 路径:$Path"
    }

    $item.Delete()
  }

  "test" {
    if ($null -eq (Get-ManagedLink $Path)) {
      exit 1
    }
  }

  "list" {
    if (-not (Test-Path -LiteralPath $Path -PathType Container)) {
      exit 0
    }

    Get-ChildItem -LiteralPath $Path -Force |
      Where-Object {
        $_.LinkType -in @("Junction", "SymbolicLink") -or
        (($_.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0 -and $_.PSIsContainer)
      } |
      ForEach-Object { $_.FullName }
  }

  "same" {
    if ([string]::IsNullOrWhiteSpace($Target)) {
      exit 1
    }

    $left = Get-ComparableFullName $Path
    $right = Get-ComparableFullName $Target
    if ($null -eq $left -or $null -eq $right) {
      exit 1
    }

    if (-not [string]::Equals($left, $right, [System.StringComparison]::OrdinalIgnoreCase)) {
      exit 1
    }
  }

  "under" {
    if ([string]::IsNullOrWhiteSpace($Target)) {
      exit 1
    }

    $child = Get-ComparableFullName $Path
    $root = Get-ComparableFullName $Target
    if ($null -eq $child -or $null -eq $root) {
      exit 1
    }

    $prefix = $root + [System.IO.Path]::DirectorySeparatorChar
    $same = [string]::Equals($child, $root, [System.StringComparison]::OrdinalIgnoreCase)
    $nested = $child.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)
    if (-not ($same -or $nested)) {
      exit 1
    }
  }
}
SKILL.md›
---
name: dbs-install-skill
description: 将单个 Skill 或 Skill 集合安装到通用 Agents、Claude Code、Codex、WorkBuddy、Grok、Hermes Agent、Kiro、Qwen Code、Cline 等 Agent。用户要求跨 Agent 安装、同步、查看、去重或卸载 Skill 时使用。
metadata:
  dbs-handoff: |
    - 一个 Skill 名称、路径或 Skill 集合目录,要安装或同步 → 在本机已有的各 Agent 里建立入口链接,本机有 Grok 时另建适配层,清理指向同一来源的重复链接,交出各入口和清理结果;目标位置已有真实目录或其他来源时停下等用户确认
    - 已安装的 Skill,要查看状态 → 安装状态和发现的重复入口
    - 已安装的 Skill,要卸载 → 移除各入口链接和 Grok 适配层,保留 Skill 源文件
    - 不交出:Skill 内容修改、项目规则文件迁移、为未安装的 Agent 新建目录、覆盖或删除真实目录
---

# dbs-install-skill:多端 Skill 安装与同步

把任意包含 `SKILL.md` 的 Skill 源目录,或包含多个 Skill 子目录的集合目录,安装到本机已经存在的 Agent。

用户始终使用同一条命令。脚本自动选择公共入口或专属入口,用户无需判断宿主类型,也无需添加模式参数。

---

## 自动路由

### 公共入口

脚本始终把 Skill 链接写入:

- 通用 Agents:`~/.agents/skills/<skill-name>`

以下客户端可以读取该公共入口,因此不再写入各自的专属目录:

- Codex;
- GitHub Copilot;
- Gemini CLI;
- Cursor;
- Augment;
- Roo Code;
- OpenCode;
- OpenHands。

同一个 Skill 不会同时出现在 `~/.agents/skills` 和上述客户端的专属目录中。这样可以避免 Codex 等客户端重复显示。

### 专属入口

以下客户端当前仍使用专属目录。只有对应主目录已经存在时,脚本才创建链接:

- Claude Code:`~/.claude/skills/<skill-name>`;
- WorkBuddy:`~/.workbuddy/skills/<skill-name>`;
- Hermes Agent:`~/.hermes/skills/<skill-name>`;
- Kiro:`~/.kiro/skills/<skill-name>`;
- Qwen Code:`~/.qwen/skills/<skill-name>`;
- Cline:`~/.cline/skills/<skill-name>`。

Unix-like 系统使用符号链接;Windows 的 Bash/MSYS 环境使用目录 Junction,避免 `ln -s` 在部分配置下生成实体目录副本。

### Grok 薄适配层

本机存在 `~/.grok` 时,脚本生成:

- `~/.grok/skills/<skill-name>/SKILL.md`

该文件必须包含 `user_invocable: true`,并指向真源 `SKILL.md`。

### 自动清理

每次执行 `link` 时,脚本同时处理历史遗留项:

1. 删除公共入口兼容客户端专属目录中指向同一真源的冗余链接;
2. 删除旧版脚本曾写入、当前已停止维护的宿主链接;
3. 删除同一宿主中指向同一真源、且规范名称已经存在的旧别名;
4. 集合安装时删除指向集合内已失效源目录的断裂链接和 Grok 适配层;
5. 保留真实目录、真实文件以及指向其他来源的链接,并报告冲突;
6. 不删除源 Skill。

---

## 核心原则

1. **一个公共入口。** 支持通用 Agents 目录的客户端统一读取 `~/.agents/skills`。
2. **必要时补专属入口。** 仅给当前仍依赖原生目录的客户端创建链接。
3. **用户无需选择模式。** 脚本不要求用户提供路由参数。
4. **公共兼容客户端只保留一份。** Codex 等客户端不能同时存在公共入口和专属入口。
5. **各宿主只使用链接。** Windows 使用目录 Junction,Unix-like 系统使用符号链接;Grok 是唯一使用薄适配层的宿主。
6. **不创建不存在的 Agent 主目录。** `~/.agents` 是公共安装入口,可以由脚本创建;其他 Agent 主目录不存在时直接跳过。
7. **不覆盖真实目录。** 目标位置已有真实目录或文件时,保留并报告;Windows 上提示它可能是旧版 MSYS 生成的实体副本。
8. **卸载只删派生产物。** `unlink` 只删除指向指定真源的符号链接或 Junction,以及本工具生成的 Grok 适配层。
9. **优先使用脚本。** 使用本 Skill 自带的 `scripts/install-skill.sh`,不要临场重写安装命令。

---

## 确定源 Skill

用户可能提供:

- Skill 名称:`dbs-diagnosis`;
- 相对路径:`skills/dbs-diagnosis`;
- 绝对路径:`/Users/.../dbskill/skills/dbs-diagnosis`;
- 外部 Skill:`/Users/.../external-skills/lark-doc`;
- Skill 集合目录:`/Users/.../dbskill/skills`;
- 当前上下文刚创建或刚修改的 Skill。

按以下优先级判断:

1. 用户给了绝对路径,直接使用;
2. 用户给了相对路径,先按当前工作目录解析,再按 dbskill 仓库根目录解析;
3. 用户只给 Skill 名称,先查当前工作目录,再查 dbskill 仓库 `skills/<name>`;
4. 用户只说“这个 Skill”,使用当前对话刚创建、改名或讨论的 Skill;
5. 仍不确定时,查看当前工作目录和仓库 `skills/` 下最近修改的 Skill;
6. 仍无法确定时,只问一句:`安装哪个 Skill?给我 Skill 名称或路径。`

源目录必须满足以下任一条件:

- 目录本身包含 `SKILL.md`;
- 目录的一级子目录中包含一个或多个 `SKILL.md`。

入口名优先读取真源 `SKILL.md` frontmatter 中的 `name`;只有 `name` 缺失时才退回源目录名。这样源目录可以使用分类前缀,各 Agent 仍保持稳定的历史触发名。

---

## 界面元数据闸门

`link` 和 `status` 执行前,脚本从源目录开始逐级向上查找 `AGENTS.md`。找到的 `AGENTS.md` 含有 `<!-- skill-interface-gate: required -->` 标记时,脚本用本 Skill 所在仓库的 `tools/check-skill-interface-metadata.py` 校验源目录:

- 校验失败时,脚本停止,不建立链接,也不查看状态;
- 校验器不存在时,脚本同样停止;
- 没有找到该标记时,脚本跳过这一步;
- `unlink` 不做这项校验。

校验失败时,把校验器的报错回报给用户,不绕过脚本手动建立链接。

---

## 执行安装

在 dbskill 仓库根目录运行:

```bash
skills/dbs-install-skill/scripts/install-skill.sh link <skill-name-or-path>
```

示例:

```bash
skills/dbs-install-skill/scripts/install-skill.sh link dbs-diagnosis
skills/dbs-install-skill/scripts/install-skill.sh link skills/my-custom-skill
skills/dbs-install-skill/scripts/install-skill.sh link skills
skills/dbs-install-skill/scripts/install-skill.sh link "/absolute/path/to/skill"
skills/dbs-install-skill/scripts/install-skill.sh link "/Users/me/external-skills"
```

执行完成后,根据脚本输出回报公共入口、专属入口、Grok 适配层和冗余清理结果。

---

## 查看状态

用户问“装好了吗”“有没有重复”“查看安装状态”时运行:

```bash
skills/dbs-install-skill/scripts/install-skill.sh status <skill-name-or-path>
```

状态正常时,脚本必须输出:

```text
✓ 未发现冗余入口
```

公共兼容客户端的专属目录中仍有同源链接时,状态返回失败并报告:

```text
✗ 发现冗余入口:<target> -> <source>
```

---

## 卸载 Skill

用户说“卸载 Skill”“取消安装”“unlink”时运行:

```bash
skills/dbs-install-skill/scripts/install-skill.sh unlink <skill-name-or-path>
```

完成后告诉用户:源 Skill 没有被删除,只移除了公共入口、专属入口和 Grok 适配层等派生产物。

---

## 输出规范

安装完成后简短回报:

```markdown
已安装 `<skill-name>`:

- 公共入口:`~/.agents/skills/<skill-name>`;
- 专属入口:仅写入本机已安装且仍需要专属目录的 Agent;
- Grok:`~/.grok/skills/<skill-name>/SKILL.md`(本机存在时);
- 去重:已清理指向同一真源的历史冗余链接。
```

遇到真实目录或其他来源时:

```markdown
已保留 `<target-path>`,因为它是一个真实目录、真实文件或指向其他来源。需要你手动确认后再处理。
```

---

## 自检

每次执行前后确认:

- 源目录存在;
- 源目录包含 `SKILL.md`,或其一级子目录包含 `SKILL.md`;
- 入口名与真源 frontmatter `name` 一致;缺失 `name` 时才使用源目录名;
- 外部路径使用绝对路径,或能从当前工作目录解析;
- `~/.agents/skills/<name>` 是公共规范入口;
- Codex 等公共兼容客户端的专属目录中没有同源链接;
- 专属宿主目标位置若存在,必须是指向当前真源的符号链接或 Junction;
- Grok 目标位置若存在,必须是本工具生成的 Grok 适配层,且指向当前真源或原真源已不存在,才允许更新或移除;
- 真实目录、真实文件和其他来源链接没有被删除;
- 源目录没有被删除;
- `private/` 与 `.private/` 没有被读取、复制、暂存或安装。

---

完成当前任务后直接结束。用户的请求超出本 Skill 的范围、任务完成后用户的材料里出现明确的后续需求,或用户询问下一步时,用一句话说明还需要哪种能力,不点名其他 Skill;当前环境已经安装 `/dbs` 时,补一句「可以输入 `/dbs`,由它匹配对应的 Skill」。