Full Code of qufei1993/skills-hub for AI

main cf6c584aa228 cached
124 files
1.1 MB
329.9k tokens
453 symbols
1 requests
Download .txt
Showing preview only (1,291K chars total). Download the full file or copy to clipboard to get everything.
Repository: qufei1993/skills-hub
Branch: main
Commit: cf6c584aa228
Files: 124
Total size: 1.1 MB

Directory structure:
gitextract_nr53l0gg/

├── .github/
│   └── workflows/
│       ├── ci.yml
│       ├── release.yml
│       └── update-featured-skills.yml
├── .gitignore
├── AGENTS.md
├── CHANGELOG.md
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── docs/
│   ├── CHANGELOG.zh.md
│   ├── README.zh.md
│   ├── future/
│   │   └── profile-requirements.md
│   ├── releases/
│   │   ├── v0.1-v0.2/
│   │   │   ├── system-design.md
│   │   │   └── system-design.zh.md
│   │   ├── v0.3.0/
│   │   │   ├── plan-bug-fixes.md
│   │   │   ├── plan-explore-page-redesign.md
│   │   │   ├── plan-featured-skills.md
│   │   │   ├── plan-online-search.md
│   │   │   └── plan-skill-detail-view.md
│   │   ├── v0.3.1/
│   │   │   ├── plan-in-app-update.md
│   │   │   ├── plan-qoderwork-support.md
│   │   │   ├── plan-settings-page.md
│   │   │   └── skills-aggregation-repo.md
│   │   ├── v0.4.0/
│   │   │   ├── bugfix-language-toggle-loading-overlay.md
│   │   │   ├── plan-in-app-update.md
│   │   │   ├── plan-qoderwork-support.md
│   │   │   ├── plan-settings-page.md
│   │   │   └── skills-aggregation-repo.md
│   │   ├── v0.4.1/
│   │   │   └── plan-frontmatter-table.md
│   │   ├── v0.4.2/
│   │   │   ├── bugfix-new-tools-modal-style.md
│   │   │   └── bugfix-root-skill-install-false-exists.md
│   │   ├── v0.4.3/
│   │   │   ├── add-copaw-support.md
│   │   │   └── bugfix-github-install-and-frontmatter.md
│   │   ├── v0.5.0/
│   │   │   ├── implementation-plan.md
│   │   │   ├── project-scope-design.md
│   │   │   └── ux-optimizations.md
│   │   └── v0.6.0/
│   │       ├── minor-updates.md
│   │       └── tag-management.md
│   ├── skills_hub_design.html
│   ├── skills_hub_v2_design.html
│   └── tag_profile_interactive_prototype.html
├── eslint.config.js
├── featured-skills.json
├── index.html
├── package.json
├── scripts/
│   ├── coverage-rust.sh
│   ├── extract-changelog.mjs
│   ├── fetch-featured-skills.mjs
│   ├── tauri-icon-desktop.mjs
│   └── version.mjs
├── src/
│   ├── App.css
│   ├── App.tsx
│   ├── components/
│   │   ├── Layout.tsx
│   │   └── skills/
│   │       ├── ExplorePage.tsx
│   │       ├── FilterBar.tsx
│   │       ├── Header.tsx
│   │       ├── LoadingOverlay.tsx
│   │       ├── SettingsPage.tsx
│   │       ├── SkillCard.tsx
│   │       ├── SkillDetailView.tsx
│   │       ├── SkillsList.tsx
│   │       ├── TagsPage.tsx
│   │       ├── modals/
│   │       │   ├── AddSkillModal.tsx
│   │       │   ├── DeleteModal.tsx
│   │       │   ├── EditSkillTagsModal.tsx
│   │       │   ├── GitPickModal.tsx
│   │       │   ├── ImportModal.tsx
│   │       │   ├── LocalPickModal.tsx
│   │       │   ├── NewToolsModal.tsx
│   │       │   ├── ScopeSyncModal.tsx
│   │       │   └── SharedDirModal.tsx
│   │       └── types.ts
│   ├── i18n/
│   │   ├── index.ts
│   │   └── resources.ts
│   ├── index.css
│   ├── main.tsx
│   ├── pages/
│   │   └── Dashboard.tsx
│   └── tauri-plugin-dialog.d.ts
├── src-tauri/
│   ├── .gitignore
│   ├── Cargo.toml
│   ├── build.rs
│   ├── capabilities/
│   │   └── default.json
│   ├── icons/
│   │   └── icon.icns
│   ├── src/
│   │   ├── commands/
│   │   │   ├── mod.rs
│   │   │   └── tests/
│   │   │       └── commands.rs
│   │   ├── core/
│   │   │   ├── cache_cleanup.rs
│   │   │   ├── cancel_token.rs
│   │   │   ├── central_repo.rs
│   │   │   ├── content_hash.rs
│   │   │   ├── featured_skills.rs
│   │   │   ├── git_fetcher.rs
│   │   │   ├── github_download.rs
│   │   │   ├── github_search.rs
│   │   │   ├── installer.rs
│   │   │   ├── mod.rs
│   │   │   ├── onboarding.rs
│   │   │   ├── skill_files.rs
│   │   │   ├── skill_store.rs
│   │   │   ├── skills_search.rs
│   │   │   ├── sync_engine.rs
│   │   │   ├── temp_cleanup.rs
│   │   │   ├── tests/
│   │   │   │   ├── central_repo.rs
│   │   │   │   ├── content_hash.rs
│   │   │   │   ├── featured_skills.rs
│   │   │   │   ├── git_fetcher.rs
│   │   │   │   ├── github_search.rs
│   │   │   │   ├── installer.rs
│   │   │   │   ├── onboarding.rs
│   │   │   │   ├── skill_store.rs
│   │   │   │   ├── skills_search.rs
│   │   │   │   ├── sync_engine.rs
│   │   │   │   ├── temp_cleanup.rs
│   │   │   │   └── tool_adapters.rs
│   │   │   └── tool_adapters/
│   │   │       └── mod.rs
│   │   ├── lib.rs
│   │   └── main.rs
│   └── tauri.conf.json
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on:
  pull_request:
  push:
    branches: [main]

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  web:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npm run version:check
      - run: npm run lint
      - run: npm run build

  rust:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: src-tauri
    steps:
      - uses: actions/checkout@v4
      - name: Install system dependencies (linux)
        run: |
          sudo apt-get update
          sudo apt-get install -y libglib2.0-dev libgtk-3-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
        with:
          workspaces: src-tauri
      - run: cargo fmt --all -- --check
      - run: cargo clippy --all-targets --all-features -- -D warnings
      - run: cargo test --all


================================================
FILE: .github/workflows/release.yml
================================================
name: Release

on:
  push:
    tags:
      - "v*"
  workflow_dispatch:

permissions:
  contents: write

concurrency:
  group: release-${{ github.ref_name }}
  cancel-in-progress: true

jobs:
  release:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        include:
          # macOS Intel (x86_64)
          - os: macos-14
            target: x86_64-apple-darwin
            arch: x86_64
          # macOS Apple Silicon (arm64)
          - os: macos-14
            target: aarch64-apple-darwin
            arch: aarch64
          # Windows x64 (Intel/AMD)
          - os: windows-2022
            target: x86_64-pc-windows-msvc
            arch: x64
          # Windows on Arm (aarch64)
          - os: windows-2022
            target: aarch64-pc-windows-msvc
            arch: arm64

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: npm

      - name: Setup Rust
        uses: dtolnay/rust-toolchain@stable

      - name: Rust cache
        uses: Swatinem/rust-cache@v2
        with:
          workspaces: src-tauri

      - name: Add macOS targets
        run: rustup target add ${{ matrix.target }}

      - name: Install macOS build deps
        if: matrix.os == 'macos-14'
        shell: bash
        run: |
          set -euxo pipefail
          brew install pkg-config cmake

      - name: Install Windows build deps
        if: matrix.os == 'windows-2022'
        shell: bash
        run: |
          set -euxo pipefail
          # Install Strawberry Perl for OpenSSL build (required by openssl-sys)
          choco install -y strawberryperl
          # Add Strawberry Perl to PATH
          export PATH="/c/Strawberry/perl/bin:$PATH"
          # WebView2 Runtime is pre-installed on windows-2022 runner
          rustup target add ${{ matrix.target }}

      - name: Install frontend deps
        run: npm ci

      - name: Prepare Tauri signing key
        shell: bash
        env:
          RAW_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
          RAW_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
        run: |
          node <<'NODE'
          const fs = require('fs');

          const raw = (process.env.RAW_KEY || '').trim();
          if (!raw) {
            console.warn('⚠️ 未配置 TAURI_SIGNING_PRIVATE_KEY,将生成未签名版本');
            process.exit(0);
          }

          const looksLikeBase64 = (s) => /^[A-Za-z0-9+/=]+$/.test(s);
          const startsWithUntrusted = (s) => s.split(/\r?\n/)[0].startsWith('untrusted comment:');

          let normalized = '';
          if (startsWithUntrusted(raw)) {
            normalized = raw.endsWith('\n') ? raw : raw + '\n';
          } else if (looksLikeBase64(raw)) {
            try {
              const decoded = Buffer.from(raw, 'base64').toString('utf8');
              if (startsWithUntrusted(decoded.trim())) {
                normalized = decoded.trimEnd() + '\n';
              } else {
                normalized = `untrusted comment: tauri signing key\n${raw.replace(/\s+/g, '')}\n`;
              }
            } catch {
              normalized = `untrusted comment: tauri signing key\n${raw.replace(/\s+/g, '')}\n`;
            }
          } else {
            try {
              const decoded = Buffer.from(raw, 'base64').toString('utf8');
              if (startsWithUntrusted(decoded.trim())) {
                normalized = decoded.trimEnd() + '\n';
              } else {
                throw new Error('not minisign');
              }
            } catch {
              console.error('❌ TAURI_SIGNING_PRIVATE_KEY 格式无法识别(不是两行原文/其 base64/单行 base64)');
              process.exit(1);
            }
          }

          const keyB64 = Buffer.from(normalized, 'utf8').toString('base64');
          const envFile = process.env.GITHUB_ENV;
          fs.appendFileSync(envFile, `TAURI_SIGNING_PRIVATE_KEY=${keyB64}\n`);

          const pwd = (process.env.RAW_PASSWORD || '').trim();
          if (pwd) fs.appendFileSync(envFile, `TAURI_SIGNING_PRIVATE_KEY_PASSWORD=${pwd}\n`);
          NODE

      - name: Import Apple certificate (codesign)
        shell: bash
        env:
          APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
          APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
          APPLE_SIGNING_IDENTITY_INPUT: ${{ secrets.APPLE_SIGNING_IDENTITY }}
          KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
        run: |
          set -euo pipefail

          if [ -z "${APPLE_CERTIFICATE:-}" ]; then
            echo "ℹ️ 未配置 APPLE_CERTIFICATE,跳过 codesign 导入"
            exit 0
          fi
          if [ -z "${APPLE_CERTIFICATE_PASSWORD:-}" ]; then
            echo "❌ 缺少 Secret:APPLE_CERTIFICATE_PASSWORD" >&2
            exit 1
          fi
          if [ -z "${KEYCHAIN_PASSWORD:-}" ]; then
            echo "❌ 缺少 Secret:KEYCHAIN_PASSWORD" >&2
            exit 1
          fi

          CERT_PATH="$RUNNER_TEMP/certificate.p12"
          KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db"

          echo "$APPLE_CERTIFICATE" | (base64 --decode 2>/dev/null || base64 -D) > "$CERT_PATH"

          security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
          security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
          security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
          security list-keychains -d user -s "$KEYCHAIN_PATH"
          security default-keychain -s "$KEYCHAIN_PATH"

          security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
          security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"

          echo "Available identities:"
          security find-identity -v -p codesigning "$KEYCHAIN_PATH" || true

          if [ -n "${APPLE_SIGNING_IDENTITY_INPUT:-}" ]; then
            echo "APPLE_SIGNING_IDENTITY=${APPLE_SIGNING_IDENTITY_INPUT}" >> "$GITHUB_ENV"
          else
            IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | head -n 1 | sed -n 's/.*\"\\(.*\\)\".*/\\1/p' || true)"
            if [ -z "$IDENTITY" ]; then
              echo "❌ 未能从 keychain 推断 APPLE_SIGNING_IDENTITY,请配置 Secret:APPLE_SIGNING_IDENTITY" >&2
              exit 1
            fi
            echo "APPLE_SIGNING_IDENTITY=$IDENTITY" >> "$GITHUB_ENV"
          fi

      - name: Build Tauri App (macOS)
        if: matrix.os == 'macos-14'
        # updater 产物(*.tar.gz/*.sig)是基于 .app bundle 生成的,因此必须包含 app
        shell: bash
        run: |
          set -euo pipefail
          echo "=== Building Tauri App for macOS ==="
          echo "Target: ${{ matrix.target }}"
          echo "Arch: ${{ matrix.arch }}"
          if [ -n "${APPLE_SIGNING_IDENTITY:-}" ]; then
            echo "✅ 使用 APPLE_SIGNING_IDENTITY=$APPLE_SIGNING_IDENTITY 进行签名"
          else
            echo "ℹ️ 未配置 APPLE_SIGNING_IDENTITY,构建未签名版本"
            unset APPLE_SIGNING_IDENTITY
          fi
          npm run tauri:build -- --target ${{ matrix.target }} --bundles app,dmg
          echo "=== Build completed ==="
          ls -la src-tauri/target/${{ matrix.target }}/release/bundle/ || true

      - name: Build Tauri App (Windows)
        if: matrix.os == 'windows-2022'
        # Windows 产物:只生成 NSIS 安装程序
        shell: bash
        run: |
          set -euo pipefail
          echo "=== Building Tauri App for Windows ==="
          echo "Target: ${{ matrix.target }}"
          echo "Arch: ${{ matrix.arch }}"
          # Add Strawberry Perl to PATH for OpenSSL build
          export PATH="/c/Strawberry/perl/bin:$PATH"
          npm run tauri:build -- --target ${{ matrix.target }} --bundles nsis --no-sign
          echo "=== Build completed ==="
          ls -la src-tauri/target/${{ matrix.target }}/release/bundle/ || true

      - name: Verify codesign (optional)
        shell: bash
        run: |
          set -euo pipefail
          if [ -z "${APPLE_CERTIFICATE:-}" ]; then
            echo "ℹ️ 未配置 APPLE_CERTIFICATE,跳过验签"
            exit 0
          fi
          TARGET="${{ matrix.target }}"
          APP_PATH="$(find "src-tauri/target/${TARGET}/release/bundle" -type d -name "*.app" | head -n 1 || true)"
          if [ -z "$APP_PATH" ]; then
            echo "⚠️ 未找到 .app,跳过验签" >&2
            exit 0
          fi
          echo "APP_PATH=$APP_PATH"
          codesign -dv --verbose=4 "$APP_PATH" || true
          codesign --verify --deep --strict --verbose=4 "$APP_PATH"

      - name: Prepare macOS Assets
        if: matrix.os == 'macos-14'
        shell: bash
        run: |
          set -euo pipefail

          mkdir -p release-assets
          VERSION="${GITHUB_REF_NAME}" # e.g. v0.1.4
          ARCH="${{ matrix.arch }}"
          TARGET="${{ matrix.target }}"

          # 产物路径在不同 target 下可能不同:优先 target/<triple>/...,兜底 target/release/...
          BUNDLE_DIRS=(
            "src-tauri/target/${TARGET}/release/bundle"
            "src-tauri/target/release/bundle"
          )

          TAR_GZ=""
          DMG=""
          for dir in "${BUNDLE_DIRS[@]}"; do
            if [ -d "${dir}" ]; then
              if [ -z "${TAR_GZ}" ]; then
                TAR_GZ="$(find "${dir}" -type f -name "*.tar.gz" | head -n 1 || true)"
              fi
              if [ -z "${DMG}" ]; then
                DMG="$(find "${dir}" -type f -name "*.dmg" | head -n 1 || true)"
              fi
            fi
          done

          if [ -z "${TAR_GZ}" ]; then
            echo "❌ 未找到 *.tar.gz updater 产物(target=${TARGET}, arch=${ARCH})。请确认 `src-tauri/tauri.conf.json` 已设置 `bundle.createUpdaterArtifacts=true`,且构建包含 `--bundles app`。" >&2
            for dir in "${BUNDLE_DIRS[@]}"; do
              echo "---- list: ${dir}" >&2
              ls -la "${dir}" 2>/dev/null || true
              find "${dir}" -maxdepth 6 -type f 2>/dev/null | head -n 200 >&2 || true
            done
            exit 1
          fi

          if [ ! -f "${TAR_GZ}.sig" ]; then
            echo "❌ 未找到 updater 签名文件:${TAR_GZ}.sig" >&2
            exit 1
          fi

          NEW_TAR_GZ="Skills-Hub-${VERSION}-macOS-${ARCH}.tar.gz"
          cp "${TAR_GZ}" "release-assets/${NEW_TAR_GZ}"
          cp "${TAR_GZ}.sig" "release-assets/${NEW_TAR_GZ}.sig"

          if [ -n "${DMG}" ]; then
            NEW_DMG="Skills-Hub-${VERSION}-macOS-${ARCH}.dmg"
            cp "${DMG}" "release-assets/${NEW_DMG}"
          else
            echo "⚠️ 未找到 macOS .dmg(可选)" >&2
          fi

      - name: Prepare Windows Assets
        if: matrix.os == 'windows-2022'
        shell: bash
        run: |
          set -euo pipefail

          mkdir -p release-assets
          VERSION="${GITHUB_REF_NAME}" # e.g. v0.1.4
          ARCH="${{ matrix.arch }}"
          TARGET="${{ matrix.target }}"

          # 产物路径在不同 target 下可能不同:优先 target/<triple>/...,兜底 target/release/...
          BUNDLE_DIRS=(
            "src-tauri/target/${TARGET}/release/bundle"
            "src-tauri/target/release/bundle"
          )

          EXE=""
          for dir in "${BUNDLE_DIRS[@]}"; do
            if [ -d "${dir}" ]; then
              if [ -z "${EXE}" ]; then
                EXE="$(find "${dir}" -type f -name "*.exe" | head -n 1 || true)"
              fi
            fi
          done

          if [ -z "${EXE}" ]; then
            echo "❌ 未找到 *.exe 安装程序(target=${TARGET}, arch=${ARCH})。请确认构建包含 `--bundles nsis`。" >&2
            for dir in "${BUNDLE_DIRS[@]}"; do
              echo "---- list: ${dir}" >&2
              ls -la "${dir}" 2>/dev/null || true
              find "${dir}" -maxdepth 6 -type f 2>/dev/null | head -n 200 >&2 || true
            done
            exit 1
          fi

          NEW_EXE="Skills-Hub-${VERSION}-Windows-${ARCH}.exe"
          cp "${EXE}" "release-assets/${NEW_EXE}"

      - name: List prepared assets
        shell: bash
        run: ls -la release-assets || true

      - name: Upload workflow artifacts
        uses: actions/upload-artifact@v4
        with:
          name: release-assets-${{ matrix.arch }}
          path: release-assets/*
          if-no-files-found: error

  assemble-updater-json:
    name: Assemble updater.json
    runs-on: macos-14
    needs: release
    permissions:
      contents: write
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Download workflow artifacts
        uses: actions/download-artifact@v4
        with:
          path: dl
          pattern: release-assets-*
          merge-multiple: true

      - name: Generate release notes from changelog
        env:
          TAG: ${{ github.ref_name }}
        shell: bash
        run: |
          set -euo pipefail
          node scripts/extract-changelog.mjs "$TAG" CHANGELOG.md > release-notes.md
          {
            echo
            echo '**Windows Note:** Windows SmartScreen may show a warning during installation. This is normal for unsigned executables. The application is safe to use.'
            echo
            echo '**macOS Note:** macOS Gatekeeper workaround (only needed on some macOS versions): `xattr -cr "/Applications/Skills Hub.app"` (https://v2.tauri.app/distribute/#macos).'
          } >> release-notes.md

      - name: Generate updater.json
        env:
          REPO: ${{ github.repository }}
          TAG: ${{ github.ref_name }}
        shell: bash
        run: |
          set -euo pipefail

          VERSION="${TAG#v}"
          PUB_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
          base_url="https://github.com/$REPO/releases/download/$TAG"

          mac_arm_url=""; mac_arm_sig=""
          mac_x64_url=""; mac_x64_sig=""

          ls -la dl || true

          shopt -s nullglob
          for sig in dl/*.sig; do
            base=${sig%.sig}
            fname=$(basename "$base")
            url="$base_url/$fname"
            sig_content=$(tr -d '\r\n' < "$sig")
            case "$fname" in
              *-macOS-aarch64.tar.gz)
                mac_arm_url="$url"; mac_arm_sig="$sig_content";;
              *-macOS-x86_64.tar.gz)
                mac_x64_url="$url"; mac_x64_sig="$sig_content";;
            esac
          done

          if [ -z "${mac_arm_url:-}" ] && [ -z "${mac_x64_url:-}" ]; then
            echo "❌ 未找到任何 macOS updater 签名(dl/*.sig); 请确认构建产物包含 *.tar.gz.sig" >&2
            exit 1
          fi

          # Read release notes if available (generated in prior step or inline)
          NOTES_FILE="release-notes.md"
          if [ -f "$NOTES_FILE" ]; then
            NOTES_CONTENT=$(cat "$NOTES_FILE")
          else
            NOTES_CONTENT="Release $TAG"
          fi
          # Escape for JSON: backslashes, double quotes, newlines
          NOTES_JSON=$(printf '%s' "$NOTES_CONTENT" | sed 's/\\/\\\\/g; s/"/\\"/g' | awk '{printf "%s\\n", $0}' | sed 's/\\n$//')

          tmp_json=$(mktemp)
          {
            echo '{'
            echo "  \"version\": \"$VERSION\","
            echo "  \"notes\": \"$NOTES_JSON\","
            echo "  \"pub_date\": \"$PUB_DATE\","
            echo '  "platforms": {'
            first=1
            if [ -n "${mac_arm_url:-}" ] && [ -n "${mac_arm_sig:-}" ]; then
              [ $first -eq 0 ] && echo ','
              echo "    \"darwin-aarch64\": {\"signature\": \"$mac_arm_sig\", \"url\": \"$mac_arm_url\"}"
              first=0
            fi
            if [ -n "${mac_x64_url:-}" ] && [ -n "${mac_x64_sig:-}" ]; then
              [ $first -eq 0 ] && echo ','
              echo "    \"darwin-x86_64\": {\"signature\": \"$mac_x64_sig\", \"url\": \"$mac_x64_url\"}"
              first=0
            fi
            echo '  }'
            echo '}'
          } > "$tmp_json"

          cat "$tmp_json"
          mv "$tmp_json" updater.json

      - name: Create/Update GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ github.ref_name }}
          name: Skills Hub ${{ github.ref_name }}
          prerelease: ${{ contains(github.ref_name, '-') }}
          body_path: release-notes.md
          files: |
            dl/*
            updater.json
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/update-featured-skills.yml
================================================
name: Update Featured Skills

on:
  schedule:
    - cron: '0 0 * * *'
  workflow_dispatch:

permissions:
  contents: write

jobs:
  update:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Fetch featured skills
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: node scripts/fetch-featured-skills.mjs

      - name: Commit if changed
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add featured-skills.json
          git diff --cached --quiet || git commit -m "chore: update featured-skills.json"
          git push


================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local
.cursor
.trae

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
coverage/
plans/

.env

bun.lock


================================================
FILE: AGENTS.md
================================================
# Skills Hub - Project Rules

## Overview

Skills Hub is a cross-platform desktop app (Tauri 2 + React 19) for managing AI Agent Skills and syncing them to 47+ AI coding tools. Core concept: "Install once, sync everywhere."

## Tech Stack

- **Frontend**: React 19 + TypeScript 5.9 (strict) + Vite 7 + Tailwind CSS 4
- **Backend**: Rust (Edition 2021, MSRV 1.77.2) + Tauri 2
- **Database**: SQLite (rusqlite, bundled)
- **Git**: libgit2 (git2 crate, vendored-openssl)
- **HTTP**: reqwest (rustls-tls, blocking)
- **i18n**: i18next (English / Chinese bilingual)
- **Notifications**: sonner (toast)
- **Icons**: lucide-react

## Common Commands

```bash
npm run dev              # Vite dev server (port 5173)
npm run tauri:dev        # Tauri dev window (frontend + backend)
npm run build            # tsc + vite build
npm run check            # Full check: lint + build + rust:fmt:check + rust:clippy + rust:test
npm run lint             # ESLint (flat config v9)
npm run rust:test        # cargo test
npm run rust:clippy      # Rust lint
npm run rust:fmt         # Rust format
npm run rust:fmt:check   # Rust format check
```

Always run `npm run check` before committing to ensure all checks pass.

## Directory Structure

```
src/                          # React frontend
├── App.tsx                   # Root component (centralized state, all modal states)
├── App.css                   # Global styles (all component styles live here)
├── index.css                 # CSS variables (theming) + Tailwind entry
├── components/
│   ├── Layout.tsx            # Main layout (sidebar + content area)
│   └── skills/               # Skills feature module
│       ├── Header.tsx        # Top bar (branding + language toggle + new button)
│       ├── FilterBar.tsx     # Filter/sort bar
│       ├── SkillsList.tsx    # Skills list container
│       ├── SkillCard.tsx     # Individual skill card
│       ├── LoadingOverlay.tsx
│       ├── types.ts          # Shared DTO type definitions (frontend ↔ backend)
│       └── modals/           # Modal components (8 total)
└── i18n/
    ├── index.ts              # i18next initialization
    └── resources.ts          # Translation resources (EN/ZH)

src-tauri/src/                # Rust backend
├── main.rs                   # Entry point (calls app_lib::run)
├── lib.rs                    # App initialization (plugin registration, DB, cleanup tasks)
├── commands/
│   ├── mod.rs                # Tauri command layer (23 commands + DTOs)
│   └── tests/
└── core/                     # Core business logic
    ├── skill_store.rs        # SQLite ORM (4 tables: skills, skill_targets, settings, discovered_skills)
    ├── installer.rs          # Skill installation (local/git, with multi-skill detection)
    ├── sync_engine.rs        # Sync engine (symlink/junction/copy triple fallback)
    ├── git_fetcher.rs        # Git clone/pull (with cache and TTL)
    ├── tool_adapters/mod.rs  # Tool adapter registry (47 AI tools)
    ├── onboarding.rs         # Existing skill scanning/discovery
    ├── github_search.rs      # GitHub API search
    ├── central_repo.rs       # Central repository path management
    ├── content_hash.rs       # SHA256 directory content hashing
    ├── cache_cleanup.rs      # Git cache cleanup
    ├── temp_cleanup.rs       # Temp directory cleanup
    └── tests/                # One test file per module (10 total)
```

## Architecture

### Frontend ↔ Backend Communication
- Uses Tauri IPC (`invoke`) to call backend commands
- Frontend call pattern: `const result = await invoke('command_name', { param })`
- Backend commands are defined in `commands/mod.rs` and registered in `lib.rs` via `generate_handler!`
- New commands must be registered in both places

### Frontend State Management
- **No state management library** — all state is centralized in `App.tsx` via `useState`
- Passed to child components via props drilling (modals receive many props)
- Data refresh pattern: call `invoke('get_managed_skills')` after operations to re-fetch the list

### Backend Layering
- `commands/` layer: Tauri command definitions, DTO conversions, error formatting (no business logic)
- `core/` layer: Pure business logic, independently testable
- Async commands use `tauri::async_runtime::spawn_blocking` to wrap synchronous operations
- Shared state injected via `app.manage(store)` + `State<'_, SkillStore>`

### Error Handling
- Backend uses `anyhow::Result<T>`, converted to string via `format_anyhow_error()` for the frontend
- Special error prefixes for frontend identification: `MULTI_SKILLS|`, `TARGET_EXISTS|`, `TOOL_NOT_INSTALLED|`
- Frontend catches with try-catch and displays errors via sonner toast

## Coding Conventions

### TypeScript
- Strict mode: `noUnusedLocals` and `noUnusedParameters` are enabled — unused variables/params cause compile errors
- Component files: PascalCase (`SkillCard.tsx`)
- Props types: `ComponentNameProps` (`SkillCardProps`)
- CSS class names: kebab-case (`modal-backdrop`, `skill-card`)
- Modal conditional rendering: `if (!open) return null` (full unmount, not display:none)
- Wrap presentational components with `memo()`
- All user-visible text must use i18n (`t('key')`), translation keys defined in `src/i18n/resources.ts`
- When adding new text, always provide both English and Chinese translations
- DTO types are defined in `src/components/skills/types.ts` and must stay in sync with the Rust DTOs in `commands/mod.rs`

### Rust
- Functions/methods: snake_case
- Constants: SCREAMING_SNAKE_CASE
- Tauri command parameters use camelCase (to match frontend JS calling convention)
- Use `anyhow::Context` to add context to errors
- New core modules must be exported in `core/mod.rs`
- Tests use `tempfile` crate for temp directories and `mockito` for HTTP mocking

### Styling
- Component styles go in `src/App.css` (not CSS Modules), using semantic CSS class names
- Theming via CSS variables + `[data-theme="dark"]` selector, variables defined in `src/index.css`
- Tailwind utility classes and custom CSS classes can be mixed

## Development Workflow

1. **Before implementing**: Briefly describe the approach and list the files to be modified. Wait for confirmation before writing code.
2. **Implement completely**: For features involving both frontend and backend, modify both sides in one pass — including Tauri command registration, DTO types, i18n translations (both EN and ZH), and UI.
3. **Verify after changes**: Always run `npm run check` after implementation to ensure lint, build, and all Rust checks pass. Fix any errors before presenting the result.
4. **Keep changes minimal**: Only modify what is necessary for the requirement. Do not refactor, add comments, or "improve" unrelated code.

## Important Notes

- Path handling must support `~` expansion (backend has `expand_home_path()`)
- Sync strategy uses triple fallback: symlink → junction (Windows) → copy
- Git uses vendored-openssl, HTTP uses rustls-tls — avoids system SSL issues
- Version numbers must stay in sync between `package.json` and `src-tauri/tauri.conf.json` (validate with `npm run version:check`)
- Rust crate is named `app_lib` (not the default package name) — use `app_lib::...` for imports
- Database has a schema migration mechanism (`migrate_legacy_db_if_needed`) — consider migrations when modifying table structures
- Tool adapter list is in `tool_adapters/mod.rs` — adding a new AI tool requires both a `ToolId` enum variant and an adapter instance


================================================
FILE: CHANGELOG.md
================================================
# Changelog

All notable changes to this project will be documented in this file.

## [Unreleased]

## [0.6.0] - 2026-05-05

### Added
- **Skill tags**: Add custom tags to managed skills for easier organization and filtering.
- **Tags page**: Manage tags from a dedicated Tags page, including create, rename, delete, and quick navigation back to filtered My Skills views.
- **Tag filtering**: Filter My Skills by one or more tags with OR matching, including a virtual `Untagged` filter for skills without tags.
- **Per-skill tag editor**: Edit a skill's tag assignments directly from the skill card.
- **Import search**: Search discovered skill candidates by name, description, or path before importing from a local directory or Git repository.

### Changed
- **My Skills filter bar**: Removed the manual refresh button; install, delete, sync, and tag-edit flows already refresh the list automatically.

### Fixed
- **Chinese filter bar layout**: Removing the refresh button fixes the cramped button layout in Chinese.
- **Discovered skills review**: The discovered skills review dialog now supports search and keeps selection counts aligned with filtered results.

## [0.5.0] - 2026-04-16

### Added
- **Project-level skill sync**: Skills can now be synced to selected project directories instead of only global tool directories.
- **Skill scope controls**: My Skills cards now show a scope badge (`Global` / project count) and include a scope modal for switching between global and project sync.
- **Scope filtering**: My Skills can be filtered by All / Global / Project scope.
- **Hermes Agent adapter**: Added global sync support for Hermes Agent via `~/.hermes/skills` ([#54](https://github.com/qufei1993/skills-hub/issues/54)).

### Changed
- **My Skills filter bar**: The section title now displays the total skill count, search is more compact, and filter controls stay on one line in the default window.
- **Default window size**: Increased the default desktop window from `800x600` to `960x680`.
- **macOS close behavior**: Closing the main window now hides it instead of quitting the app; reopening from the Dock restores and focuses the window.
- **Project sync support matrix**: Project-level sync is now treated as an explicit per-tool capability; tools without a confirmed project skills directory are global-only.

### Fixed
- **Import takeover for identical skills**: Importing an existing skill can now safely take over same-name targets when the content hash matches.
- **Unsynced tool re-enable entry**: Tool buttons that were unsynced from a skill remain visible so they can be re-enabled.
- **SKILL.md metadata parsing**: YAML block scalar descriptions in frontmatter now render correctly in skill cards and detail views.

## [0.4.3] - 2026-04-11

### Added
- **Copaw tool adapter**: Support for Copaw AI coding tool (thanks @LeonDevLifeLog [PR#50](https://github.com/qufei1993/skills-hub/pull/50)).

### Fixed
- **Git skill install & frontmatter rendering**: Fixed issues with Git-based skill installation and frontmatter metadata rendering.
- **Git skill discovery for container paths**: Fixed skill discovery failing when repository uses container-style directory paths.

## [0.4.2] - 2026-04-06

### Fixed
- **New tools modal style**: "New tools detected" dialog now uses consistent header/footer structure (`modal-header` + `modal-footer`) matching all other modals, fixing missing padding and border separators ([#46](https://github.com/qufei1993/skills-hub/issues/46)).
- **Git skill name derivation**: Installing a Git skill from a repo root (subpath `"."`) now correctly derives the name from the repository URL instead of using `"."` as the display name.

## [0.4.1] - 2026-03-21

### Added
- **Frontmatter metadata table**: Markdown files with YAML frontmatter now render a GitHub-style metadata table at the top of the skill detail view.

## [0.4.0] - 2026-03-20

### Added
- **In-app update check**: Check for updates directly within Settings, download and install without leaving the app ([#33](https://github.com/qufei1993/skills-hub/issues/33)).
- **QoderWork tool adapter**: Support for QoderWork desktop AI agent (`~/.qoderwork/skills/`) ([#34](https://github.com/qufei1993/skills-hub/issues/34)).

### Changed
- **Settings promoted to full page**: Settings moved from a modal dialog to a dedicated page view, consistent with My Skills / Explore navigation pattern.
- **Curated skills aggregation**: Explore page now sources skills from a curated list of 7 high-quality repositories.

### Fixed
- Language toggle briefly flashing "Installing Skills..." loading overlay on Explore page.

## [0.3.0] - 2026-03-15

### Added
- **Explore page**: Explore promoted from a modal tab to an independent page with My Skills / Explore top-level navigation.
- **Featured skills**: Explore page displays curated skills from ClawHub API (updated daily via GitHub Actions) with frontend filtering and one-click install.
- **Online skill search**: Real-time search via skills.sh API (triggered at 2+ characters, 500ms debounce), results deduplicated against the featured list and shown in separate sections.
- **Skill detail view**: Click a skill name to browse its files with a file tree, Markdown rendering (GFM + frontmatter stripping), and syntax highlighting (40+ languages, light/dark theme adaptive).
- **Skill description field**: Description extracted from SKILL.md frontmatter at install time, stored in database, and displayed on My Skills cards.
- **GitHub Token setting**: Optional GitHub Token input in settings to increase API rate limit from 60 to 5,000 requests/hour.
- **MoltBot tool adapter**: Added standalone MoltBot tool support after OpenClaw rename/split.

### Fixed
- Git install deriving skill name as "skills" when URL points to a `skills/` subdirectory, causing duplicated sync paths ([#28](https://github.com/qufei1993/skills-hub/issues/28)).
- GitHub API rate-limit errors now display the exact reset time instead of a generic message.
- Windows "Access Denied" OS error 5 when syncing to tools ([#20](https://github.com/qufei1993/skills-hub/issues/20)).
- Git repo directory structures not correctly recognized as skills ([#18](https://github.com/qufei1993/skills-hub/issues/18), [#8](https://github.com/qufei1993/skills-hub/issues/8)).
- Repos using `.claude/skills/` directory format not detected ([#27](https://github.com/qufei1993/skills-hub/issues/27)).
- OpenClaw path updated from `.moltbot/skills` to `.openclaw/skills` ([#29](https://github.com/qufei1993/skills-hub/issues/29)).

### Changed
- My Skills list: tool badges now only show synced tools, collapsing to `+N more` beyond 5.
- Manual Add modal simplified to Local Directory / Git Repository tabs only (Explore tab removed).
- Multi-skill repo online install now auto-matches target skill (exact → unique-contains → fallback to manual picker).

## [0.2.0] - 2026-02-01

### Added
- **Windows platform support**: Full support for Windows build and release (thanks @jrtxio [PR#6](https://github.com/qufei1993/skills-hub/pull/6)).
- Support and display for many new tools (e.g., Kimi Code CLI, Augment, OpenClaw, Cline, CodeBuddy, Command Code, Continue, Crush, Junie, iFlow CLI, Kiro CLI, Kode, MCPJam, Mistral Vibe, Mux, OpenClaude IDE, OpenHands, Pi, Qoder, Qwen Code, Trae/Trae CN, Zencoder, Neovate, Pochi, AdaL).
- UI confirmation and linked selection for tools that share the same global skills directory.
- Local import multi-skill discovery aligned with Git rules, with a selection list and invalid-item reasons.
- New local import commands for listing candidates and installing a selected subpath with SKILL.md validation.

### Changed
- Antigravity global skills directory updated to `~/.gemini/antigravity/global_skills`.
- OpenCode global skills directory corrected to `~/.config/opencode/skills`.
- Tool status now includes `skills_dir`; frontend tool list/sync is driven by backend data and deduped by directory.
- Sync/unsync now updates records across tools sharing a skills directory to avoid duplicate filesystem work and inconsistent state.
- Local import flow now scans candidates first; single valid candidate installs directly, multi-candidate opens selection.

## [0.1.1] - 2026-01-26

### Changed
- GitHub Actions release workflow for macOS packaging and uploading `updater.json` (`.github/workflows/release.yml`).
- Cursor sync now always uses directory copy due to Cursor not following symlinks when discovering skills: https://forum.cursor.com/t/cursor-doesnt-follow-symlinks-to-discover-skills/149693/4
- Managed skill update now re-syncs copy-mode targets using copy-only overwrite, and forces Cursor targets to copy to avoid accidental relinking.

## [0.1.0] - 2026-01-25

### Added
- Initial release of Skills Hub desktop app (Tauri + React).
- Central repository for Skills; sync to multiple AI coding tools (symlink/junction preferred, copy fallback).
- Local import from folders.
- Git import via repository URL or folder URL (`/tree/<branch>/<path>`), with multi-skill selection and batch install.
- Sync and update: copy-mode targets can be refreshed; managed skills can be updated from source.
- Migration intake: scan existing tool directories, import into central repo, and one‑click sync.
- New tool detection and optional sync.
- Basic settings: storage path, language, and theme.
- Git cache with cleanup (days) and freshness window (seconds).

### Build & Release
- Local packaging scripts for macOS (dmg), Windows (msi/nsis), Linux (deb/appimage).
- GitHub Actions build validation and tag-based draft releases (release notes pulled from `CHANGELOG.md`).

### Performance
- Git import and batch install optimizations: cached clones reduce repeated fetches; timeouts and non‑interactive git improve stability.


================================================
FILE: CLAUDE.md
================================================
Read and follow all instructions in [AGENTS.md](./AGENTS.md).


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Code of Conduct

This project follows the Contributor Covenant Code of Conduct (v2.1).

## Our Pledge

We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.

## Our Standards

Examples of behavior that contributes to a positive environment for our
community include:

- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
  and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
  overall community

Examples of unacceptable behavior include:

- The use of sexualized language or imagery, and sexual attention or advances
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
  address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Enforcement Responsibilities

Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.

Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.

## Scope

This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
`qzfweb@gmail.com`.

All complaints will be reviewed and investigated promptly and fairly.

## Enforcement Guidelines

Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:

### 1. Correction

**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.

**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.

### 2. Warning

**Community Impact**: A violation through a single incident or series
of actions.

**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.

### 3. Temporary Ban

**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.

**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.

### 4. Permanent Ban

**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.

**Consequence**: A permanent ban from any sort of public interaction within the
community.

## Attribution

This Code of Conduct is adapted from the Contributor Covenant, version 2.1,
available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing

Thanks for taking the time to contribute to Skills Hub!

## Development Requirements

- Node.js 18+ (recommended: 20+)
- Rust (stable)
- Tauri system dependencies (install per the official Tauri docs for macOS/Windows/Linux)

## Run Locally

```bash
npm install
npm run tauri:dev
```

## Quality Checks

```bash
npm run lint
npm run build
```

## Run Unit Tests

Rust unit tests live under `src-tauri/src/core/tests/`.

```bash
cd src-tauri
cargo test
```

## Before Submitting a PR

- Ensure `npm run lint` and `npm run build` pass
- Ensure `cd src-tauri && cargo test` pass
- Keep changes small and focused (do not commit local configs/caches/build artifacts)
- For UI changes, include screenshots or a short recording

## Reporting Issues

Please include the following in your issue report:

- OS version (macOS/Windows/Linux)
- Skills Hub version
- Steps to reproduce and expected vs. actual behavior
- Relevant logs (please redact local paths and any sensitive information)


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2026 Skills Hub contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Skills Hub (Tauri Desktop)

A cross-platform desktop app (Tauri + React) to manage Agent Skills in one place and sync them to multiple AI coding tools’ global or project-level skills directories (prefer symlink/junction, fallback to copy) — “Install once, sync everywhere”.

## Documentation

- English (default): `README.md` (this file)
- 中文:[`docs/README.zh.md`](docs/README.zh.md)

## Key Features

- **Explore page**: Browse curated featured skills and search online — one-click install & sync to all detected tools
- **Tags page**: Create, rename, and delete custom tags, then jump back to matching skills from one dedicated view
- **Tag filtering**: Organize skills with multiple tags and filter My Skills by tag, including `Untagged` skills
- **Global / project sync**: Sync skills globally across all projects, or scope them to selected project directories
- **Scope controls**: Switch a skill between Global and Project scope, manage project directories, and filter My Skills by scope
- **Skill detail view**: Click a skill name to browse its files with Markdown rendering and syntax highlighting (40+ languages)
- **Unified view**: Managed skills, total skill count, scope badges, and per-tool activation status
- **Onboarding migration**: Scan existing skills in installed tools, import into the Central Repo, and sync
- **Import sources**: Local folder / Git URL (including searchable multi-skill repo selection, `.claude/skills/` directory support)
- **Update**: Refresh from source; propagate updates to copy-mode targets
- **New tool detection**: Detect newly installed tools and prompt to sync managed skills

### My Skills
![My Skills](docs/assets/my-skills.png)

### Explore & Search
![Explore](docs/assets/explore-search.png)

### Manual Add
![Manual Add](docs/assets/manual-add.png)

### Skill Detail
![Skill Detail](docs/assets/skill-detail.png)

## Supported AI Coding Tools

Project skills dirs are relative to the selected project root. Tools marked `N/A` do not have a confirmed project-level skills directory and are supported for global sync only.

| tool key | Display name | global skills dir (relative to `~`) | project skills dir (relative to project) | detected if exists (relative to `~`) |
| --- | --- | --- | --- | --- |
| `cursor` | Cursor | `.cursor/skills` | `.agents/skills` | `.cursor` |
| `claude_code` | Claude Code | `.claude/skills` | `.claude/skills` | `.claude` |
| `codex` | Codex | `.codex/skills` | `.agents/skills` | `.codex` |
| `opencode` | OpenCode | `.config/opencode/skills` | `.agents/skills` | `.config/opencode` |
| `antigravity` | Antigravity | `.gemini/antigravity/skills` | `.agents/skills` | `.gemini/antigravity` |
| `amp` | Amp | `.config/agents/skills` | `.agents/skills` | `.config/agents` |
| `kimi_cli` | Kimi Code CLI | `.config/agents/skills` | `.agents/skills` | `.config/agents` |
| `augment` | Augment | `.augment/skills` | `.augment/skills` | `.augment` |
| `openclaw` | OpenClaw | `.openclaw/skills` | `skills` | `.openclaw` |
| `copaw` | Copaw | `.copaw/skill_pool` | `.copaw/skill_pool` | `.copaw` |
| `cline` | Cline | `.agents/skills` | `.agents/skills` | `.agents` |
| `codebuddy` | CodeBuddy | `.codebuddy/skills` | `.codebuddy/skills` | `.codebuddy` |
| `command_code` | Command Code | `.commandcode/skills` | `.commandcode/skills` | `.commandcode` |
| `continue` | Continue | `.continue/skills` | `.continue/skills` | `.continue` |
| `crush` | Crush | `.config/crush/skills` | `.crush/skills` | `.config/crush` |
| `junie` | Junie | `.junie/skills` | `.junie/skills` | `.junie` |
| `iflow_cli` | iFlow CLI | `.iflow/skills` | `.iflow/skills` | `.iflow` |
| `kiro_cli` | Kiro CLI | `.kiro/skills` | `.kiro/skills` | `.kiro` |
| `kode` | Kode | `.kode/skills` | `.kode/skills` | `.kode` |
| `mcpjam` | MCPJam | `.mcpjam/skills` | `.mcpjam/skills` | `.mcpjam` |
| `mistral_vibe` | Mistral Vibe | `.vibe/skills` | `.vibe/skills` | `.vibe` |
| `mux` | Mux | `.mux/skills` | `.mux/skills` | `.mux` |
| `openclaude` | OpenClaude IDE | `.openclaude/skills` | `.openclaude/skills` | `.openclaude` |
| `openhands` | OpenHands | `.openhands/skills` | `.openhands/skills` | `.openhands` |
| `pi` | Pi | `.pi/agent/skills` | `.pi/skills` | `.pi` |
| `qoder` | Qoder | `.qoder/skills` | `.qoder/skills` | `.qoder` |
| `qoderwork` | QoderWork | `.qoderwork/skills` | `.qoderwork/skills` | `.qoderwork` |
| `qwen_code` | Qwen Code | `.qwen/skills` | `.qwen/skills` | `.qwen` |
| `trae` | Trae | `.trae/skills` | `.trae/skills` | `.trae` |
| `trae_cn` | Trae CN | `.trae-cn/skills` | `.trae/skills` | `.trae-cn` |
| `zencoder` | Zencoder | `.zencoder/skills` | `.zencoder/skills` | `.zencoder` |
| `neovate` | Neovate | `.neovate/skills` | `.neovate/skills` | `.neovate` |
| `pochi` | Pochi | `.pochi/skills` | `.pochi/skills` | `.pochi` |
| `adal` | AdaL | `.adal/skills` | `.adal/skills` | `.adal` |
| `kilo_code` | Kilo Code | `.kilocode/skills` | `.kilocode/skills` | `.kilocode` |
| `roo_code` | Roo Code | `.roo/skills` | `.roo/skills` | `.roo` |
| `goose` | Goose | `.config/goose/skills` | `.goose/skills` | `.config/goose` |
| `gemini_cli` | Gemini CLI | `.gemini/skills` | `.agents/skills` | `.gemini` |
| `github_copilot` | GitHub Copilot | `.copilot/skills` | `.agents/skills` | `.copilot` |
| `clawdbot` | Clawdbot | `.clawdbot/skills` | `.clawdbot/skills` | `.clawdbot` |
| `droid` | Droid | `.factory/skills` | `.factory/skills` | `.factory` |
| `windsurf` | Windsurf | `.codeium/windsurf/skills` | `.windsurf/skills` | `.codeium/windsurf` |
| `moltbot` | MoltBot | `.moltbot/skills` | `.moltbot/skills` | `.moltbot` |
| `hermes_agent` | Hermes Agent | `.hermes/skills` | N/A | `.hermes` |

## Development

### Prerequisites

- Node.js 18+ (recommended: 20+)
- Rust (stable)
- Tauri system dependencies (follow Tauri official docs for your OS)

```bash
npm install
npm run tauri:dev
```

### Build

```bash
npm run lint
npm run build
npm run tauri:build
```

#### Platform build commands (from `package.json`)

- macOS (dmg): `npm run tauri:build:mac:dmg`
- macOS (universal dmg): `npm run tauri:build:mac:universal:dmg`
- Windows (MSI): `npm run tauri:build:win:msi`
- Windows (NSIS exe): `npm run tauri:build:win:exe`
- Windows (MSI+NSIS): `npm run tauri:build:win:all`
- Linux (deb): `npm run tauri:build:linux:deb`
- Linux (AppImage): `npm run tauri:build:linux:appimage`
- Linux (deb+AppImage): `npm run tauri:build:linux:all`

### Tests (Rust)

```bash
cd src-tauri
cargo test
```

## Contributing & Security

- Contributing: [`CONTRIBUTING.md`](CONTRIBUTING.md)
- Code of Conduct: [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md)
- Security: [`SECURITY.md`](SECURITY.md)

## FAQ / Notes

- Where are skills stored? The Central Repo defaults to `~/.skillshub` (configurable in Settings).
- What are tags for? Tags help you find and organize skills. They do not change where a skill is synced or which tools can use it.
- What is project-level sync? The skill is still stored once in the Central Repo, but its sync target is a selected project directory such as `<project>/.agents/skills`, `<project>/.claude/skills`, or another tool-specific project skills path.
- Why is Cursor sync always copy? Cursor currently does not support symlink/junction-based skill directories, so Skills Hub forces directory copy when syncing to Cursor.
- Why does sync sometimes fall back to copy? Skills Hub prefers symlink/junction, but on some systems (especially Windows) symlinks may be restricted; in that case it falls back to directory copy.
- What does `TARGET_EXISTS|...` mean? The target folder already exists and the operation did not overwrite it (default is non-destructive). Remove the existing folder or retry with the appropriate overwrite flow.
- macOS Gatekeeper note (unsigned/notarized builds, may vary by macOS version): if you see “damaged” or “unverified developer”, run `xattr -cr "/Applications/Skills Hub.app"` (https://v2.tauri.app/distribute/#macos).

## Supported Platforms

- macOS (verified)
- Windows (expected by design; not validated locally)
- Linux (expected by design; not validated locally)

## License

MIT License — see `LICENSE`.


================================================
FILE: SECURITY.md
================================================
# Security Policy

## Supported Versions

Only the latest code on the `main` branch is supported.

## Reporting a Vulnerability

If you discover a security vulnerability (e.g., arbitrary file read/write, path traversal, permission bypass, information disclosure, or command injection), please do not disclose it in a public issue.

Please report it via one of the following channels:

- GitHub Security Advisories (recommended): create a private report via the repository's Security → Advisories page
- Or email: `qzfweb@gmail.com`

We will confirm receipt and provide a remediation plan as soon as possible. Please keep details private until a fix is released.


================================================
FILE: docs/CHANGELOG.zh.md
================================================
# 更新日志

本文件记录项目的重要变更(中文版本)。

## [Unreleased]

## [0.6.0] - 2026-05-05

### 新增
- **Skill 标签**:可为已托管 Skill 添加自定义标签,方便整理和筛选。
- **标签页面**:新增独立 Tags / 标签页面,支持新建、重命名、删除标签,并可快速跳回已筛选的 My Skills 视图。
- **标签筛选**:My Skills 支持按一个或多个标签筛选,使用 OR 匹配;同时提供虚拟 `Untagged` / `无标签` 筛选项。
- **单个 Skill 标签编辑**:可直接从 Skill 卡片打开标签编辑入口,调整该 Skill 的标签关联。
- **导入搜索**:从本地目录或 Git 仓库导入前,可按名称、描述或路径搜索候选 Skill。

### 变更
- **My Skills 筛选栏**:移除手动刷新按钮;安装、删除、同步和编辑标签等流程已自动刷新列表。

### 修复
- **中文筛选栏布局**:移除刷新按钮后,修复中文界面下按钮区域拥挤和样式错乱问题。
- **发现 Skill 审核弹窗**:查看已发现 Skills 时支持搜索,并让选择数量与筛选结果保持一致。

## [0.5.0] - 2026-04-16

### 新增
- **项目级 Skill 同步**:Skill 现在可以同步到指定项目目录,不再只支持同步到各工具的全局目录。
- **同步范围控制**:My Skills 卡片新增范围徽标(全局 / 项目数量),并提供范围弹窗用于切换全局同步和项目同步。
- **范围筛选**:My Skills 支持按全部 / 全局 / 项目范围筛选。
- **Hermes Agent 工具适配**:新增 Hermes Agent 全局同步支持,目录为 `~/.hermes/skills`([#54](https://github.com/qufei1993/skills-hub/issues/54))。

### 变更
- **My Skills 筛选栏**:标题现在显示 Skill 总数,搜索框更紧凑,默认窗口下筛选控件保持单行展示。
- **默认窗口尺寸**:桌面端默认窗口从 `800x600` 调整为 `960x680`。
- **macOS 关闭行为**:点击主窗口关闭按钮现在隐藏窗口而不是退出应用;从 Dock 重新打开时会恢复并聚焦窗口。
- **项目级同步支持矩阵**:项目级同步改为按工具显式声明;未确认项目级 skills 目录的工具仅作为全局同步目标。

### 修复
- **同名同内容 Skill 导入接管**:导入已有 Skill 时,如果目标同名目录内容一致,现在可以安全接管同步状态。
- **取消同步后的工具重新启用入口**:从 Skill 取消同步的工具按钮会继续显示,便于重新启用。
- **SKILL.md 元数据解析**:正确解析 frontmatter 中的 YAML block scalar 描述,并在卡片和详情页正常展示。

## [0.4.3] - 2026-04-11

### 新增
- **Copaw 工具适配**:新增 Copaw AI 编程工具支持(感谢 @LeonDevLifeLog [PR#50](https://github.com/qufei1993/skills-hub/pull/50))。

### 修复
- **Git 技能安装与 frontmatter 渲染**:修复 Git 技能安装及 frontmatter 元数据渲染问题。
- **Git 技能发现(容器路径)**:修复仓库使用容器风格目录路径时技能发现失败的问题。

## [0.4.2] - 2026-04-06

### 修复
- **检测到新工具弹窗样式**:「New tools detected」弹窗改用与其他弹窗一致的 `modal-header` + `modal-footer` 结构,修复标题缺少内边距和分隔线的问题([#46](https://github.com/qufei1993/skills-hub/issues/46))。
- **Git 技能名称推导**:从仓库根目录(subpath 为 `"."`)安装 Git 技能时,现在正确从仓库 URL 推导名称,不再以 `"."` 作为展示名称。

## [0.4.1] - 2026-03-21

### 新增
- **Frontmatter 元数据表格**:包含 YAML frontmatter 的 Markdown 文件在技能详情页顶部以 GitHub 风格的表格展示元数据。

## [0.4.0] - 2026-03-20

### 新增
- **应用内检查更新**:在设置页内直接检查新版本,支持下载安装,无需手动访问 GitHub Releases([#33](https://github.com/qufei1993/skills-hub/issues/33))。
- **QoderWork 工具适配**:新增 QoderWork 桌面 AI 代理支持(`~/.qoderwork/skills/`)([#34](https://github.com/qufei1993/skills-hub/issues/34))。

### 变更
- **设置页面化**:设置从模态弹窗升级为独立页面视图,与 My Skills / Explore 导航风格一致。
- **精选技能聚合**:Explore 数据源改为 7 个精选高质量仓库。

### 修复
- 切换语言时 Explore 页面短暂闪现「Installing Skills...」加载遮罩。

## [0.3.0] - 2026-03-15

### 新增
- **Explore 页面**:探索功能从弹窗提升为独立页面,顶部导航新增 My Skills / Explore 两个页面级 Tab 切换。
- **精选技能推荐**:Explore 页展示由 ClawHub API 预生成的热门技能列表(GitHub Actions 每日更新),支持前端筛选和一键安装。
- **在线技能搜索**:输入 ≥ 2 字符后通过 skills.sh API 实时搜索,500ms 防抖,搜索结果与精选列表自动去重、分区展示。
- **技能详情页**:点击技能名称进入详情视图,支持文件树浏览、Markdown 渲染(GFM + frontmatter 剥离)和代码语法高亮(40+ 语言,亮/暗主题自适应)。
- **技能描述字段**:安装时从 SKILL.md frontmatter 提取 description 存入数据库,My Skills 卡片展示描述文本。
- **GitHub Token 配置**:设置页新增可选的 GitHub Token 输入,认证后 API 限额从 60 提升至 5000 次/小时。
- **MoltBot 工具适配**:OpenClaw 更名拆分后新增独立的 MoltBot 工具支持。

### 修复
- Git 安装时 skill 名称为 "skills" 导致同步路径重复([#28](https://github.com/qufei1993/skills-hub/issues/28))。
- GitHub API 限流错误未提示重置时间,现在显示具体重置时间。
- Windows 同步时拒绝访问 OS error 5([#20](https://github.com/qufei1993/skills-hub/issues/20))。
- Git 仓库目录结构无法被正确识别为 skill([#18](https://github.com/qufei1993/skills-hub/issues/18)、[#8](https://github.com/qufei1993/skills-hub/issues/8))。
- 不支持 `.claude/skills/` 目录格式的仓库([#27](https://github.com/qufei1993/skills-hub/issues/27))。
- OpenClaw 路径更新(`.moltbot/skills` → `.openclaw/skills`)([#29](https://github.com/qufei1993/skills-hub/issues/29))。

### 变更
- My Skills 列表优化:工具徽章只显示已同步的工具,超过 5 个折叠为 `+N more`。
- 添加技能弹窗(Manual Add)精简为仅保留 Local Directory / Git Repository 两个 Tab。
- 多技能仓库在线安装时支持自动匹配(精确 → 唯一包含 → 回退手动选择)。

## [0.2.0] - 2026-02-01
### 新增
- **Windows 平台支持**:支持 Windows 构建与发布(感谢 @jrtxio [PR#6](https://github.com/qufei1993/skills-hub/pull/6))。
- 新增多款工具适配与显示(如 Kimi Code CLI、Augment、OpenClaw、Cline、CodeBuddy、Command Code、Continue、Crush、Junie、iFlow CLI、Kiro CLI、Kode、MCPJam、Mistral Vibe、Mux、OpenClaude IDE、OpenHands、Pi、Qoder、Qwen Code、Trae/Trae CN、Zencoder、Neovate、Pochi、AdaL 等)。
- 前端新增共享技能目录提示与联动选择:同一全局 skills 目录的工具勾选/同步/取消同步会一起生效,并弹窗确认。
- 本地导入对齐 Git 规则的 multi-skill 发现,支持批量选择并展示无效项原因。
- 新增本地导入候选列表/按子路径安装的命令,并在安装前校验 SKILL.md。

### 变更
- Antigravity 默认全局技能目录更新为 `~/.gemini/antigravity/global_skills`。
- OpenCode 全局技能目录修正为 `~/.config/opencode/skills`。
- 工具状态接口增加 `skills_dir` 字段,前端列表与同步逻辑改为后端驱动并按目录去重。
- 同一 skills 目录的工具在同步/取消同步时统一写入与清理记录,避免重复文件操作与状态不一致。
- 本地导入流程改为先扫描候选:单个有效候选直接安装,多个候选进入选择列表。

## [0.1.1] - 2026-01-26

### 变更
- GitHub Actions 发版工作流:macOS 打包并上传 `updater.json`(`.github/workflows/release.yml`)。
- Cursor 同步固定使用 Copy:因为 Cursor 在发现 skills 时不会跟随 symlink:https://forum.cursor.com/t/cursor-doesnt-follow-symlinks-to-discover-skills/149693/4
- 托管技能更新时:对 copy 模式目标使用“纯 copy 覆盖回灌”;并对 Cursor 目标强制回灌为 copy,避免误创建软链导致不可用。

## [0.1.0] - 2026-01-24

### 新增
- Skills Hub 桌面应用(Tauri + React)初始发布。
- Skills 中心仓库:统一托管并同步到多种 AI 编程工具(优先 symlink/junction,失败回退 copy)。
- 本地导入:支持从本地文件夹导入 Skill。
- Git 导入:支持仓库 URL/文件夹 URL(`/tree/<branch>/<path>`),支持多 Skill 候选选择与批量安装。
- 同步与更新:copy 模式目标支持回灌更新;托管技能支持从来源更新。
- 迁移接管:扫描工具目录中已有 Skills,导入中心仓库并可一键同步。
- 新工具检测并可选择同步。
- 基础设置:存储路径、界面语言、主题模式。
- Git 缓存:支持按天清理与新鲜期(秒)配置。

### 构建与发布
- 本地打包脚本:macOS(dmg)、Windows(msi/nsis)、Linux(deb/appimage)。
- GitHub Actions 跨平台构建验证与 tag 发布 Draft Release(从 `CHANGELOG.md` 自动提取发布说明)。

### 性能
- Git 导入/批量安装优化:缓存 clone 减少重复拉取;增加超时与无交互提示提升稳定性。


================================================
FILE: docs/README.zh.md
================================================
# Skills Hub(Tauri Desktop)

一个跨平台桌面应用(Tauri + React),用于统一管理 Agent Skills,并把它们同步到多种 AI 编程工具的全局或项目级 skills 目录(优先 symlink/junction,失败回退 copy),实现 “Install once, sync everywhere”。

> English documentation: [`README.md`](../README.md)

## 主要功能

- **Explore 探索页**:独立页面浏览精选技能推荐和在线搜索,一键安装并同步到所有已检测工具
- **Tags 标签页**:在独立页面中新建、重命名、删除自定义标签,并快速跳转到对应的 Skill 列表
- **标签筛选**:为 Skill 添加多个标签,并在 My Skills 中按标签筛选,包括查看 `无标签` Skill
- **全局 / 项目级同步**:Skill 可同步到全局目录,在所有项目中生效;也可限定到指定项目目录中生效
- **同步范围控制**:在全局和项目范围之间切换 Skill,管理项目目录,并按范围筛选 My Skills
- **技能详情页**:点击技能名称查看完整文件内容,支持文件树浏览、Markdown 渲染和代码语法高亮(40+ 语言)
- **统一视图**:查看 Hub 托管的 skills 总数、范围徽标及其在各工具的生效状态
- **迁移接管**:扫描本机工具目录已有 skills,导入到中心仓库并可一键同步
- **多来源导入**:本地目录 / Git 仓库 URL(含可搜索的 multi-skill 候选选择、`.claude/skills/` 目录格式支持)
- **更新**:从原来源更新中心仓库内容,并回灌 copy 模式的目标
- **新工具检测**:发现新安装工具时提示是否同步所有已托管 skills

### My Skills — 技能管理列表
![My Skills](./assets/my-skills.png)

### Explore — 探索与在线搜索
![Explore](./assets/explore-search.png)

### Manual Add — 手动添加技能
![Manual Add](./assets/manual-add.png)

### Skill Detail — 技能详情与文件浏览
![Skill Detail](./assets/skill-detail.png)

## 支持的 AI 编程工具

项目级 skills 目录相对所选项目根目录。标记为“不支持”的工具尚未确认项目级 skills 目录,仅支持全局同步。

| tool key | 工具 | 全局 skills 目录(相对 `~`) | 项目级 skills 目录(相对项目根目录) | 存在即视为已安装(相对 `~`) |
| --- | --- | --- | --- | --- |
| `cursor` | Cursor | `.cursor/skills` | `.agents/skills` | `.cursor` |
| `claude_code` | Claude Code | `.claude/skills` | `.claude/skills` | `.claude` |
| `codex` | Codex | `.codex/skills` | `.agents/skills` | `.codex` |
| `opencode` | OpenCode | `.config/opencode/skills` | `.agents/skills` | `.config/opencode` |
| `antigravity` | Antigravity | `.gemini/antigravity/skills` | `.agents/skills` | `.gemini/antigravity` |
| `amp` | Amp | `.config/agents/skills` | `.agents/skills` | `.config/agents` |
| `kimi_cli` | Kimi Code CLI | `.config/agents/skills` | `.agents/skills` | `.config/agents` |
| `augment` | Augment | `.augment/skills` | `.augment/skills` | `.augment` |
| `openclaw` | OpenClaw | `.openclaw/skills` | `skills` | `.openclaw` |
| `copaw` | Copaw | `.copaw/skill_pool` | `.copaw/skill_pool` | `.copaw` |
| `cline` | Cline | `.agents/skills` | `.agents/skills` | `.agents` |
| `codebuddy` | CodeBuddy | `.codebuddy/skills` | `.codebuddy/skills` | `.codebuddy` |
| `command_code` | Command Code | `.commandcode/skills` | `.commandcode/skills` | `.commandcode` |
| `continue` | Continue | `.continue/skills` | `.continue/skills` | `.continue` |
| `crush` | Crush | `.config/crush/skills` | `.crush/skills` | `.config/crush` |
| `junie` | Junie | `.junie/skills` | `.junie/skills` | `.junie` |
| `iflow_cli` | iFlow CLI | `.iflow/skills` | `.iflow/skills` | `.iflow` |
| `kiro_cli` | Kiro CLI | `.kiro/skills` | `.kiro/skills` | `.kiro` |
| `kode` | Kode | `.kode/skills` | `.kode/skills` | `.kode` |
| `mcpjam` | MCPJam | `.mcpjam/skills` | `.mcpjam/skills` | `.mcpjam` |
| `mistral_vibe` | Mistral Vibe | `.vibe/skills` | `.vibe/skills` | `.vibe` |
| `mux` | Mux | `.mux/skills` | `.mux/skills` | `.mux` |
| `openclaude` | OpenClaude IDE | `.openclaude/skills` | `.openclaude/skills` | `.openclaude` |
| `openhands` | OpenHands | `.openhands/skills` | `.openhands/skills` | `.openhands` |
| `pi` | Pi | `.pi/agent/skills` | `.pi/skills` | `.pi` |
| `qoder` | Qoder | `.qoder/skills` | `.qoder/skills` | `.qoder` |
| `qoderwork` | QoderWork | `.qoderwork/skills` | `.qoderwork/skills` | `.qoderwork` |
| `qwen_code` | Qwen Code | `.qwen/skills` | `.qwen/skills` | `.qwen` |
| `trae` | Trae | `.trae/skills` | `.trae/skills` | `.trae` |
| `trae_cn` | Trae CN | `.trae-cn/skills` | `.trae/skills` | `.trae-cn` |
| `zencoder` | Zencoder | `.zencoder/skills` | `.zencoder/skills` | `.zencoder` |
| `neovate` | Neovate | `.neovate/skills` | `.neovate/skills` | `.neovate` |
| `pochi` | Pochi | `.pochi/skills` | `.pochi/skills` | `.pochi` |
| `adal` | AdaL | `.adal/skills` | `.adal/skills` | `.adal` |
| `kilo_code` | Kilo Code | `.kilocode/skills` | `.kilocode/skills` | `.kilocode` |
| `roo_code` | Roo Code | `.roo/skills` | `.roo/skills` | `.roo` |
| `goose` | Goose | `.config/goose/skills` | `.goose/skills` | `.config/goose` |
| `gemini_cli` | Gemini CLI | `.gemini/skills` | `.agents/skills` | `.gemini` |
| `github_copilot` | GitHub Copilot | `.copilot/skills` | `.agents/skills` | `.copilot` |
| `clawdbot` | Clawdbot | `.clawdbot/skills` | `.clawdbot/skills` | `.clawdbot` |
| `droid` | Droid | `.factory/skills` | `.factory/skills` | `.factory` |
| `windsurf` | Windsurf | `.codeium/windsurf/skills` | `.windsurf/skills` | `.codeium/windsurf` |
| `moltbot` | MoltBot | `.moltbot/skills` | `.moltbot/skills` | `.moltbot` |
| `hermes_agent` | Hermes Agent | `.hermes/skills` | 不支持 | `.hermes` |

完整路径规则与检测逻辑见 [`src-tauri/src/core/tool_adapters/mod.rs`](../src-tauri/src/core/tool_adapters/mod.rs)。

## 开发

### 环境要求

- Node.js 18+(建议 20+)
- Rust(stable)
- Tauri 系统依赖(按官方文档安装)

### 启动(桌面端)

```bash
npm install
npm run tauri:dev
```

### 构建

```bash
npm run lint
npm run build
npm run tauri:build
```

#### 各系统构建命令(来自 `package.json`)

- macOS(dmg):`npm run tauri:build:mac:dmg`
- macOS(universal dmg):`npm run tauri:build:mac:universal:dmg`
- Windows(MSI):`npm run tauri:build:win:msi`
- Windows(NSIS exe):`npm run tauri:build:win:exe`
- Windows(MSI+NSIS):`npm run tauri:build:win:all`
- Linux(deb):`npm run tauri:build:linux:deb`
- Linux(AppImage):`npm run tauri:build:linux:appimage`
- Linux(deb+AppImage):`npm run tauri:build:linux:all`

### 测试(Rust)

```bash
cd src-tauri
cargo test
```

## FAQ / 备注

- Skill 存在哪里?中心仓库(Central Repo)默认是 `~/.skillshub`,可在设置里修改。
- 标签用于什么?标签只用于查找和整理 Skill,不会改变 Skill 的同步目录,也不会改变哪些工具可以使用它。
- 什么是项目级同步?Skill 仍然只在中心仓库保存一份,但同步目标变为指定项目目录,例如 `<project>/.agents/skills`、`<project>/.claude/skills` 或其它工具对应的项目级 skills 路径。
- Cursor 为什么强制 Copy?Cursor 当前不支持软链(symlink/junction)形式的技能目录,因此同步到 Cursor 时会固定使用目录复制(copy)。
- 为什么有时会变成 Copy?默认优先 symlink/junction,但在某些系统(尤其 Windows)可能因为权限/策略导致无法创建链接,会自动回退到目录复制。
- `TARGET_EXISTS|...` 是什么意思?目标目录已存在且默认不覆盖(为了安全)。你需要先清理目标目录,或在“接管/覆盖”的明确流程里重试。
- macOS Gatekeeper 备注(未签名/未公证构建,不同 macOS 版本表现可能不同):如提示“已损坏/无法验证开发者”,可执行 `xattr -cr "/Applications/Skills Hub.app"`(https://v2.tauri.app/distribute/#macos)。

## 支持的系统

- macOS(已验证)
- Windows(按架构应支持,未做本地验证)
- Linux(按架构应支持,未做本地验证)

## License

MIT License(见 `LICENSE`)。


================================================
FILE: docs/future/profile-requirements.md
================================================
# Skill Profile / 配置方案后续需求记录

## 背景

GitHub Issue #23 提到“技能分组”能力:

- Skill 作为底层资产存在。
- 单个 Skill 可以被划分到多个组中复用。
- 组支持快速挂载与切换。
- 可根据不同项目和不同工具组合不同 Skill。

这个方向和 v0.6.0 的标签功能有关联,但不属于同一个需求。

标签解决的是:

```text
如何查找和整理 Skill。
```

Profile / 配置方案解决的是:

```text
如何让一套 Skill 作为当前工作场景实际生效。
```

因此 Profile 不纳入 v0.6.0,单独记录为后续评估需求。

---

## 建议命名

不建议使用 `Group / 分组` 作为主 UI 名称。

原因:

- “分组”容易被用户理解成分类展示。
- 它和 `Tag / 标签` 的语义过近。
- 如果同时出现“标签”和“分组”,用户容易混淆。

建议名称:

```text
Profile / 配置方案
```

中文 UI 可使用:

```text
配置方案
```

英文 UI 可使用:

```text
Profile
```

一句话定义:

```text
Profile 是一套可应用的 Skill 同步配置。
```

---

## 与 Tag 的区别

| 概念 | 目的 | 是否影响同步 | 是否可多选 |
|------|------|--------------|------------|
| Tag | 查找、筛选、整理 Skill | 否 | 是 |
| Profile | 应用一套 Skill 配置 | 是 | 建议否 |

核心区分:

```text
Tag 用于找 Skill。
Profile 用于用 Skill。
```

---

## 初步产品规则

### 1. Profile 建议单选激活

第一版建议只允许一个 Active Profile。

原因:

- Profile 表示当前工作场景配置。
- 多个 Profile 同时启用会让语义接近“标签组叠加”。
- 多 Profile 会引入工具目标合并、冲突处理和预览复杂度。

建议模型:

```text
Current Profile: Skills Hub Dev
```

切换 Profile 是替换当前同步配置,不是叠加。

### 2. Profile 内部可以包含多个 Skill

一个 Profile 可以包含多个 Skill:

```text
Skills Hub Dev
- react
- tauri-desktop
- test-driven-development
- frontend-design
```

### 3. 一个 Skill 可以属于多个 Profile

Skill 是可复用资产。

例如:

```text
frontend-design
- Skills Hub Dev
- Review Flow
- Docs Writing
```

同一个 Profile 内不能重复包含同一个 Skill。

数据层建议使用:

```sql
UNIQUE(profile_id, skill_id)
```

### 4. Profile 可以配置目标工具

Profile 可能需要记录目标工具:

```text
Profile: Skills Hub Dev
Skills: react, tauri-desktop, test-driven-development
Tools: Cursor, Codex, Claude Code
```

是否在第一版实现目标工具配置,需要后续结合现有同步模型评估。

### 5. 需要处理未加入任何 Profile 的 Skill

类似标签中的 `Untagged`,Profile 维度也可能存在:

```text
Unassigned
```

含义:

```text
没有加入任何 Profile 的 Skill。
```

`Unassigned` 不是真实 Profile,而是系统虚拟状态。

后续 UI 可在 Profiles 页面顶部提示:

```text
3 skills are not in any profile        [Review]
```

---

## 关键交互问题

后续实现前需要确认以下问题。

### 1. Profile 是否直接影响同步结果

需要明确:

- 应用 Profile 时是否会新增同步。
- 应用 Profile 时是否会移除不在 Profile 中的旧同步。
- 是否只影响当前 Profile 的目标工具。
- 是否影响全局同步和项目级同步。

### 2. 切换 Profile 是否需要预览

建议需要。

示例:

```text
Apply Docs Writing?

+ 2 skills will be added
- 3 skills will be removed
= 1 skill will stay active

[Cancel] [Apply Profile]
```

切换 Profile 涉及实际同步变更,不应该静默执行。

### 3. Profile 是否绑定项目

Issue #23 提到“针对不同项目”。

需要评估:

- Profile 是否绑定项目路径。
- 进入某项目时是否自动推荐 Profile。
- Profile 与 v0.5.0 项目级同步如何协作。
- 项目切换是否自动应用 Profile。

第一版建议不要自动应用,先做手动切换。

### 4. 是否允许多个 Profile 同时启用

当前建议不允许。

如果后续确实需要多 Profile,应明确合并规则:

- Skill 集合是并集还是交集。
- 工具集合如何合并。
- 同步移除如何判断。
- 冲突时谁优先。

在没有清晰规则前,不建议支持多 Profile 同时启用。

---

## 可能的 UI 方向

### My Skills 顶部

仅展示当前配置:

```text
Current Profile: Skills Hub Dev ▾
```

选择其他 Profile 后弹出变更预览。

### Profiles 页面

```text
Profiles                                      [+ New Profile]

左侧列表:
- Skills Hub Dev      Active
- Docs Writing
- Review Flow

右侧详情:
Skills Hub Dev
React + Tauri + Rust workspace

Tools
[Cursor] [Codex] [Claude Code]

Skills
[✓] react
[✓] tauri-desktop
[✓] test-driven-development
[ ] youtube-transcript

[Preview Changes] [Apply Profile]
```

### Unassigned 处理

Profiles 页面顶部:

```text
3 skills are not in any profile        [Review]
```

点击 `Review` 后展示未分配 Skill,并允许加入某个 Profile。

---

## 数据模型草案

仅供后续评估,不作为当前实现承诺。

```sql
CREATE TABLE skill_profiles (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL UNIQUE,
  description TEXT,
  is_active INTEGER NOT NULL DEFAULT 0,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL
);

CREATE TABLE skill_profile_links (
  profile_id INTEGER NOT NULL,
  skill_id INTEGER NOT NULL,
  created_at TEXT NOT NULL,
  PRIMARY KEY (profile_id, skill_id),
  FOREIGN KEY (profile_id) REFERENCES skill_profiles(id) ON DELETE CASCADE,
  FOREIGN KEY (skill_id) REFERENCES skills(id) ON DELETE CASCADE
);

CREATE TABLE skill_profile_tools (
  profile_id INTEGER NOT NULL,
  tool TEXT NOT NULL,
  created_at TEXT NOT NULL,
  PRIMARY KEY (profile_id, tool),
  FOREIGN KEY (profile_id) REFERENCES skill_profiles(id) ON DELETE CASCADE
);
```

如果坚持单一 Active Profile,需要在应用层保证同一时间只有一个 `is_active = 1`。

---

## 暂不实现内容

在需求没有进一步确认前,暂不实现:

- Profile 创建 / 编辑。
- Profile 切换。
- Profile 自动应用到项目。
- 多 Profile 同时启用。
- Profile 与同步目标的合并规则。
- Profile 未分配 Skill 的批量处理。

---

## 后续评估结论要求

进入实现前,至少需要明确:

- Profile 是否直接改变同步结果。
- 切换 Profile 的删除策略。
- 是否绑定项目路径。
- 是否配置目标工具。
- 是否只允许一个 Active Profile。
- 未加入任何 Profile 的 Skill 如何处理。

这些问题明确前,Profile 不应和 Tag 放在同一版本交付。


================================================
FILE: docs/releases/v0.1-v0.2/system-design.md
================================================
# Skills Hub (Tauri Desktop) — System Design

This document describes the system design of **Skills Hub**, aligned with the current repository implementation.

> 中文版:[`docs/system-design.zh.md`](docs/system-design.zh.md)

## 1. Background

AI coding tools (Cursor, Claude Code, Codex, etc.) often support “Skills/Agents/Tools”, but each tool stores global skills in a different location. This causes duplicated installs, drift, and a lack of a unified view.

Skills Hub solves this by storing skill contents in a **Central Repo** and mapping them into each tool via **symlink/junction/copy** — “Install once, sync everywhere”.

## 2. Goals

- Unified view of managed skills and per-tool activation
- Onboarding migration: scan existing skills in installed tools, group by name, detect conflicts via content hash, then import & sync
- Import sources: local folder and Git URLs (including multi-skill repo selection)
- Update: refresh central content from source; propagate updates to copy-mode targets
- Tool detection: detect newly installed tools and prompt to sync
- Configurable Central Repo path (default `~/.skillshub`)

## 3. Glossary

- **Skill**: a directory-based capability package (typically contains `SKILL.md`)
- **Managed Skill**: a skill stored in the Central Repo and indexed in SQLite
- **Central Repo**: canonical storage directory, default `~/.skillshub`
- **Tool/Agent**: a supported AI coding tool with a global skills directory
- **Target**: per-tool mapping of a managed skill (symlink/junction/copy), stored in `skill_targets`
- **Fingerprint / Content Hash**: directory hash used to detect identical vs conflicting variants

## 4. Architecture

### 4.1 System Context

```mermaid
flowchart TB
  user["User"]
  app["Skills Hub Desktop App\n(Tauri + React)"]

  fsTools["Tool global skills directories\n~/.cursor/skills, etc."]
  fsCentral["Central Repo\n~/.skillshub"]
  db["SQLite\nskills_hub.db"]
  cache["Cache\nskills-hub-git-*"]
  gh["GitHub / Git Remote"]

  user -->|manage/import/sync| app
  app <--> fsTools
  app <--> fsCentral
  app <--> db
  app <--> cache
  app <-->|clone/search| gh
```

### 4.2 Containers

```mermaid
flowchart LR
  subgraph WebView[Frontend: React + Vite]
    ui["UI Components\n(App/Modals/SkillCard)"]
    invoke["invoke(command,args)"]
    ui --> invoke
  end

  subgraph Tauri[Backend: Rust + Tauri]
    cmd["commands/*\nDTO + spawn_blocking"]
    core["core/*\ninstaller/sync/store/onboarding"]
    cmd --> core
  end

  invoke --> cmd
  core --> db["SQLite"]
  core --> fsCentral["Central Repo"]
  core --> fsTools["Tools Skills Dirs"]
  core --> cache["Cache Temp Git Dirs"]
```

## 5. Storage Design

### 5.1 Filesystem

- Central Repo (default): `~/.skillshub`
- Git imports: clone into cache temp, then copy into Central Repo (Central Repo does not store `.git`)
- Tool mapping: write into each tool’s skills directory via symlink/junction/copy

### 5.2 SQLite

DB path: `app_data_dir()/skills_hub.db`

Main tables:

- `skills`: managed skills in the Central Repo (source_type/source_ref/central_path/content_hash/updated_at, etc.)
- `skill_targets`: per-tool activation state (tool/target_path/mode/status/synced_at)
- `settings`: key/value settings (e.g., central repo path, installed tools set)

```mermaid
erDiagram
  skills ||--o{ skill_targets : "id = skill_id"
  skills {
    TEXT id PK
    TEXT name
    TEXT source_type
    TEXT source_ref
    TEXT source_revision
    TEXT central_path UK
    TEXT content_hash
    INTEGER created_at
    INTEGER updated_at
    INTEGER last_sync_at
    INTEGER last_seen_at
    TEXT status
  }
  skill_targets {
    TEXT id PK
    TEXT skill_id FK
    TEXT tool
    TEXT target_path
    TEXT mode
    TEXT status
    TEXT last_error
    INTEGER synced_at
  }
```

## 6. Supported Tools

Adapter definitions: `src-tauri/src/core/tool_adapters/mod.rs`.

| tool key | Display name | skills dir (relative to `~`) | detect dir (relative to `~`) |
| --- | --- | --- | --- |
| `cursor` | Cursor | `.cursor/skills` | `.cursor` |
| `claude_code` | Claude Code | `.claude/skills` | `.claude` |
| `codex` | Codex | `.codex/skills` | `.codex` |
| `opencode` | OpenCode | `.config/opencode/skills` | `.config/opencode` |
| `antigravity` | Antigravity | `.gemini/antigravity/global_skills` | `.gemini/antigravity` |
| `amp` | Amp | `.config/agents/skills` | `.config/agents` |
| `kimi_cli` | Kimi Code CLI | `.config/agents/skills` | `.config/agents` |
| `augment` | Augment | `.augment/rules` | `.augment` |
| `openclaw` | OpenClaw | `.moltbot/skills` | `.moltbot` |
| `cline` | Cline | `.cline/skills` | `.cline` |
| `codebuddy` | CodeBuddy | `.codebuddy/skills` | `.codebuddy` |
| `command_code` | Command Code | `.commandcode/skills` | `.commandcode` |
| `continue` | Continue | `.continue/skills` | `.continue` |
| `crush` | Crush | `.config/crush/skills` | `.config/crush` |
| `junie` | Junie | `.junie/skills` | `.junie` |
| `iflow_cli` | iFlow CLI | `.iflow/skills` | `.iflow` |
| `kiro_cli` | Kiro CLI | `.kiro/skills` | `.kiro` |
| `kode` | Kode | `.kode/skills` | `.kode` |
| `mcpjam` | MCPJam | `.mcpjam/skills` | `.mcpjam` |
| `mistral_vibe` | Mistral Vibe | `.vibe/skills` | `.vibe` |
| `mux` | Mux | `.mux/skills` | `.mux` |
| `openclaude` | OpenClaude IDE | `.openclaude/skills` | `.openclaude` |
| `openhands` | OpenHands | `.openhands/skills` | `.openhands` |
| `pi` | Pi | `.pi/agent/skills` | `.pi` |
| `qoder` | Qoder | `.qoder/skills` | `.qoder` |
| `qwen_code` | Qwen Code | `.qwen/skills` | `.qwen` |
| `trae` | Trae | `.trae/skills` | `.trae` |
| `trae_cn` | Trae CN | `.trae-cn/skills` | `.trae-cn` |
| `zencoder` | Zencoder | `.zencoder/skills` | `.zencoder` |
| `neovate` | Neovate | `.neovate/skills` | `.neovate` |
| `pochi` | Pochi | `.pochi/skills` | `.pochi` |
| `adal` | AdaL | `.adal/skills` | `.adal` |
| `kilo_code` | Kilo Code | `.kilocode/skills` | `.kilocode` |
| `roo_code` | Roo Code | `.roo/skills` | `.roo` |
| `goose` | Goose | `.config/goose/skills` | `.config/goose` |
| `gemini_cli` | Gemini CLI | `.gemini/skills` | `.gemini` |
| `github_copilot` | GitHub Copilot | `.copilot/skills` | `.copilot` |
| `clawdbot` | Clawdbot | `.clawdbot/skills` | `.clawdbot` |
| `droid` | Droid | `.factory/skills` | `.factory` |
| `windsurf` | Windsurf | `.codeium/windsurf/skills` | `.codeium/windsurf` |

## 7. Command Contract (overview)

Commands are exposed from `src-tauri/src/commands/mod.rs` and invoked from the frontend.

Key commands:

- `get_central_repo_path`, `set_central_repo_path`
- `get_tool_status`, `get_onboarding_plan`, `get_managed_skills`
- `install_local`, `install_git`, `list_git_skills_cmd`, `install_git_selection`
- `sync_skill_to_tool`, `unsync_skill_from_tool`
- `update_managed_skill`, `delete_managed_skill`

Frontend-visible error prefixes:

- `MULTI_SKILLS|...`
- `TARGET_EXISTS|<path>`
- `TOOL_NOT_INSTALLED|<tool>`

## 8. Key UX Flows (summary)

- Startup: load central repo path, tool status, onboarding plan, and managed skills list.
- Import discovered skills: copy a selected variant into the Central Repo, then sync to selected tools (with safe overwrite rules).
- Update: rebuild central content from source; resync copy-mode targets.


================================================
FILE: docs/releases/v0.1-v0.2/system-design.zh.md
================================================
# Skills Hub(Tauri Desktop)系统设计文档

> English version: [`docs/system-design.md`](system-design.md)

> 基于当前仓库实现(commit `b5246ab`),结合历史计划与 UI 设计稿整理,目标是给后来维护者提供一份“能落地、可对照代码”的完整系统设计说明。  

## 1. 背景与问题定义

现代 AI 编程工具(如 Cursor、Claude Code、Codex 等)往往使用“Skills/Agents/Tools”机制扩展能力,但各工具的全局 skills 目录分散在用户主目录下不同位置,导致:

- **无法统一查看**:用户很难知道“我有哪些 Skill、在哪些工具生效、版本是否一致”。
- **重复安装与漂移**:同一个 Skill 被复制到多个工具目录,更新后不一致。
- **迁移成本高**:安装 Skills Hub 后,用户机器上可能已存在大量 skills,需要安全接管并去重。

Skills Hub 的核心思路是将 Skill 内容集中存放在“中心仓库(Central Repo)”,并把各工具目录中的技能以 **symlink/junction/copy** 的方式映射到中心仓库,实现 “Install once, sync everywhere”。

## 2. 目标与非目标

### 2.1 目标(当前实现覆盖)

- **统一视图**:列出 Hub 托管的 skills、其来源(local/git)以及对各工具的生效状态。
- **多工具同步**:对已安装工具,在其默认 skills 目录生成映射(优先链接,失败回退复制)。
- **迁移接管(Onboarding)**:扫描已安装工具目录中的已有 skills,按名称聚合并通过目录指纹检测冲突,提供导入与同步能力。
- **多来源导入**:
  - 本地目录导入(复制到中心仓库并入库)
  - Git 导入(支持 GitHub repo URL 与 folder URL;支持 multi-skill 仓库的候选选择)
  - GitHub 搜索(后端已实现,前端入口当前为 disabled)
- **更新**:按来源(git/local)重建中心目录;对 copy 模式的目标回灌更新。
- **工具动态检测**:启动时检测“新安装工具”,提示是否一键同步已托管 skills。
- **可配置中心仓库路径**:默认 `~/.skillshub`。

### 2.2 非目标(当前版本不做/不保证)

- 不做复杂的版本并存(例如 `name@cursor`)与多版本依赖解析。
- 不保证对“用户手工维护的外部 symlink 链接”做完整接管策略(仅在扫描中识别为 link 并展示)。
- 不提供云同步/多设备同步能力(当前为本机文件系统 + SQLite)。
- 不包含自动定时更新(设置文案有“24h auto-update”的占位,但当前未落地定时任务)。

## 3. 术语与核心概念

- **Skill**:一个以目录为单位的能力包,通常包含 `SKILL.md` 等文件。
- **Managed Skill**:被 Skills Hub 托管并持久化到 SQLite 的 Skill(中心目录为权威内容)。
- **Central Repo(中心仓库)**:Hub 存放 skills 内容的中心目录,默认 `~/.skillshub`(可配置)。
- **Tool/Agent**:一个支持 skills 的 AI 工具(cursor/claude_code/codex/...),每个工具有默认 skills 目录。
- **Target(同步目标)**:某个 managed skill 在某个工具目录里的映射结果(symlink/junction/copy),对应 DB 表 `skill_targets`。
- **Onboarding Plan**:首次/手动扫描得到的“候选导入集合”,按 skill name 聚合为 group,并在冲突时提供 variant 选择。
- **Fingerprint/Content Hash**:对 skill 目录计算的哈希(忽略 `.git` 等),用于判断不同工具里同名 skill 是否同内容。

## 4. 总体架构(C4 风格)

> 为了便于快速建立“脑内地图”,本章提供若干 Mermaid 架构/流程图。GitHub/多数 Markdown 预览器可直接渲染;若你的编辑器不支持,可用 Mermaid Preview 插件查看。

### 4.1 系统上下文(Context)

- 用户通过桌面应用管理本机 skills。
- 应用需要读写:
  - 用户主目录下各工具的默认 skills 目录(以及 detect 目录)
  - 中心仓库目录(默认 `~/.skillshub`)
  - 应用数据目录中的 SQLite DB(`skills_hub.db`)
  - 应用缓存目录中的 git 临时 clone 目录(`skills-hub-git-*`)

```mermaid
flowchart TB
  user["用户"]
  app["Skills Hub 桌面应用\n(Tauri + React)"]

  fsTools["各工具全局 Skills 目录\n~/.cursor/skills 等"]
  fsCentral["中心仓库\n~/.skillshub"]
  db["SQLite\nskills_hub.db"]
  cache["Cache\nskills-hub-git-*"]
  gh["GitHub / 任意 Git 仓库"]

  user -->|管理/导入/同步| app
  app <--> fsTools
  app <--> fsCentral
  app <--> db
  app <--> cache
  app <-->|clone/search| gh
```

### 4.2 容器(Containers)

- **Frontend(WebView)**:React + Vite,负责 UI/交互、i18n、主题切换、调用 Tauri commands。
- **Backend(Tauri Rust)**:提供文件系统操作、Git 拉取、SQLite 持久化、工具适配与扫描、同步引擎等能力。
- **SQLite(嵌入式)**:`rusqlite`(bundled)存储托管技能与同步状态等。

```mermaid
flowchart LR
  subgraph WebView[Frontend: React + Vite]
    ui["UI Components\n(App/Modals/SkillCard)"]
    i18n["i18n + Theme"]
    invoke["invoke(command,args)"]
    ui --> invoke
    ui --> i18n
  end

  subgraph Tauri[Backend: Rust + Tauri]
    cmd["commands/*\nDTO + spawn_blocking\nerror formatting"]
    core["core/*\ninstaller/sync/store/onboarding"]
    cmd --> core
  end

  invoke --> cmd
  core --> db["SQLite"]
  core --> fsCentral["Central Repo"]
  core --> fsTools["Tools Skills Dirs"]
  core --> cache["Cache Temp Git Dirs"]
  core --> gh["GitHub API / Git clone"]
```

### 4.3 组件(Components)

前端(`src/`):

- `src/main.tsx`:入口,初始化 i18n,渲染 `App`。
- `src/App.tsx`:单页 Dashboard,聚合状态与业务流程(扫描/导入/同步/更新/删除/设置)。
- `src/components/skills/*`:Header、FilterBar、SkillCard、SkillsList、各类 Modal。
- `src/i18n/*`:i18next 资源与初始化。
- `src/index.css`、`src/App.css`:设计稿风格的 token + 组件样式(支持 light/dark)。

后端(`src-tauri/src/`):

- `src-tauri/src/lib.rs`:Tauri Builder,初始化 DB、注册 commands、启动临时目录清理任务。
- `src-tauri/src/commands/mod.rs`:Tauri commands 对外接口层(线程隔离、错误格式化、DTO)。
- `src-tauri/src/core/*`:核心业务模块(见 6 章)。

## 5. 数据与存储设计

### 5.1 文件系统布局

#### 中心仓库

- 默认路径:`~/.skillshub`(`src-tauri/src/core/central_repo.rs`)
- 每个 Skill 使用一个目录:`<central_repo>/<skill_name>/`
- 特性:
  - **不存完整 git repo**:git 导入使用临时 clone,再把内容复制进中心目录,避免中心目录包含 `.git`。
  - **名称即目录名**:默认取来源目录名 / repo 名 / subpath 末段;允许用户在导入时指定 display name。

#### Git 临时目录(缓存)

- 位置:Tauri `app_cache_dir()`(OS 特定)
- 命名:`skills-hub-git-<uuid>`
- 安全标记:写入 marker 文件 `.skills-hub-git-temp`
- 清理策略:应用启动后台 best-effort 清理超过 24h 的目录(仅匹配 prefix + marker)

#### 工具目录

每个 tool adapter 提供:

- `relative_skills_dir`:全局 skills 目录(相对 home)
- `relative_detect_dir`:用于判断工具是否“已安装”的目录(相对 home)

例如 Cursor:

- detect:`~/.cursor`
- skills:`~/.cursor/skills`

当前支持的 AI 编程工具(Tool Adapters,见 `src-tauri/src/core/tool_adapters/mod.rs`)如下:

| tool key | 显示名称 | skills 目录(相对 `~`) | detect 目录(相对 `~`) |
| --- | --- | --- | --- |
| `cursor` | Cursor | `.cursor/skills` | `.cursor` |
| `claude_code` | Claude Code | `.claude/skills` | `.claude` |
| `codex` | Codex | `.codex/skills` | `.codex` |
| `opencode` | OpenCode | `.config/opencode/skills` | `.config/opencode` |
| `antigravity` | Antigravity | `.gemini/antigravity/global_skills` | `.gemini/antigravity` |
| `amp` | Amp | `.config/agents/skills` | `.config/agents` |
| `kimi_cli` | Kimi Code CLI | `.config/agents/skills` | `.config/agents` |
| `augment` | Augment | `.augment/rules` | `.augment` |
| `openclaw` | OpenClaw | `.moltbot/skills` | `.moltbot` |
| `cline` | Cline | `.cline/skills` | `.cline` |
| `codebuddy` | CodeBuddy | `.codebuddy/skills` | `.codebuddy` |
| `command_code` | Command Code | `.commandcode/skills` | `.commandcode` |
| `continue` | Continue | `.continue/skills` | `.continue` |
| `crush` | Crush | `.config/crush/skills` | `.config/crush` |
| `junie` | Junie | `.junie/skills` | `.junie` |
| `iflow_cli` | iFlow CLI | `.iflow/skills` | `.iflow` |
| `kiro_cli` | Kiro CLI | `.kiro/skills` | `.kiro` |
| `kode` | Kode | `.kode/skills` | `.kode` |
| `mcpjam` | MCPJam | `.mcpjam/skills` | `.mcpjam` |
| `mistral_vibe` | Mistral Vibe | `.vibe/skills` | `.vibe` |
| `mux` | Mux | `.mux/skills` | `.mux` |
| `openclaude` | OpenClaude IDE | `.openclaude/skills` | `.openclaude` |
| `openhands` | OpenHands | `.openhands/skills` | `.openhands` |
| `pi` | Pi | `.pi/agent/skills` | `.pi` |
| `qoder` | Qoder | `.qoder/skills` | `.qoder` |
| `qwen_code` | Qwen Code | `.qwen/skills` | `.qwen` |
| `trae` | Trae | `.trae/skills` | `.trae` |
| `trae_cn` | Trae CN | `.trae-cn/skills` | `.trae-cn` |
| `zencoder` | Zencoder | `.zencoder/skills` | `.zencoder` |
| `neovate` | Neovate | `.neovate/skills` | `.neovate` |
| `pochi` | Pochi | `.pochi/skills` | `.pochi` |
| `adal` | AdaL | `.adal/skills` | `.adal` |
| `kilo_code` | Kilo Code | `.kilocode/skills` | `.kilocode` |
| `roo_code` | Roo Code | `.roo/skills` | `.roo` |
| `goose` | Goose | `.config/goose/skills` | `.config/goose` |
| `gemini_cli` | Gemini CLI | `.gemini/skills` | `.gemini` |
| `github_copilot` | GitHub Copilot | `.copilot/skills` | `.copilot` |
| `clawdbot` | Clawdbot | `.clawdbot/skills` | `.clawdbot` |
| `droid` | Droid | `.factory/skills` | `.factory` |
| `windsurf` | Windsurf | `.codeium/windsurf/skills` | `.codeium/windsurf` |

备注:
- 工具“是否安装”的判断规则:detect 目录存在即认为已安装(`is_tool_installed`)。
- 扫描 Codex 的 skills 时会过滤目录名 `.system`(避免把系统内置技能当作可迁移对象)。

### 5.2 SQLite 数据模型

DB 文件路径:`app_data_dir()/skills_hub.db`(`src-tauri/src/core/skill_store.rs`)

```mermaid
erDiagram
  skills ||--o{ skill_targets : "id = skill_id"
  skills {
    TEXT id PK
    TEXT name
    TEXT source_type
    TEXT source_ref
    TEXT source_revision
    TEXT central_path UK
    TEXT content_hash
    INTEGER created_at
    INTEGER updated_at
    INTEGER last_sync_at
    INTEGER last_seen_at
    TEXT status
  }
  skill_targets {
    TEXT id PK
    TEXT skill_id FK
    TEXT tool
    TEXT target_path
    TEXT mode
    TEXT status
    TEXT last_error
    INTEGER synced_at
  }
  settings {
    TEXT key PK
    TEXT value
  }
  discovered_skills {
    TEXT id PK
    TEXT tool
    TEXT found_path
    TEXT name_guess
    TEXT fingerprint
    INTEGER found_at
    TEXT imported_skill_id FK
  }
```

#### 表:`skills`

代表 Hub 托管的技能(中心仓库权威)。

- `id`:UUID
  - `name`:展示名称(也用于中心目录名)
  - `source_type`:`local` | `git`
  - `source_ref`:本地路径或 URL
  - `source_revision`:git commit(若可得)
  - `central_path`:中心目录路径(unique)
  - `content_hash`:目录 fingerprint(可为空)
  - `created_at` / `updated_at` / `last_sync_at` / `last_seen_at`
  - `status`:目前主要使用 `ok`

#### 表:`skill_targets`

每条记录代表一个 skill 在某个工具中的生效映射。

- `skill_id` + `tool` 唯一
- `target_path`:工具目录中的路径(最终路径)
- `mode`:`auto` | `symlink` | `junction` | `copy`
- `status` / `last_error` / `synced_at`

#### 表:`settings`

key/value 存储:

- `central_repo_path`:中心仓库路径(可选)
- `installed_tools_v1`:最近一次检测到的已安装工具 key 列表(JSON)
- `onboarding_completed`:当前实现提供 set/get 接口,但 Onboarding 是否完成逻辑尚未作为 gating 条件使用(可作为后续增强点)

#### 表:`discovered_skills`

当前 schema 存在,但前端/后端主流程使用的是“运行时扫描生成 plan”,并未将扫描结果落库(预留后续增强)。

## 6. 后端核心模块设计(Rust)

> 代码集中在 `src-tauri/src/core/*`,commands 仅做线程隔离/DTO/错误格式化。

### 6.1 Tool Adapters(工具适配层)

文件:`src-tauri/src/core/tool_adapters/mod.rs`

职责:

- 定义 `ToolId` 与 `ToolAdapter`(display name、skills 路径、detect 路径)。
- `is_tool_installed()`:通过 detect 目录存在性判断。
- `scan_tool_dir()`:遍历 skills 目录下的一级子目录作为 skill 名;Codex 额外过滤 `.system`。
- `detect_link()`:用 `symlink_metadata/read_link` 尝试识别链接,并返回 `is_link/link_target`(用于 Onboarding 展示)。

### 6.2 Onboarding(扫描与聚合)

文件:`src-tauri/src/core/onboarding.rs`

流程:

1. 遍历所有 adapters,跳过未安装工具。
2. 扫描 tools 的 skills 目录得到 `DetectedSkill` 列表。
3. 对每个 detected skill 计算 `fingerprint = hash_dir(path)`(忽略 `.git` 等)。
4. 按 `skill.name` 聚合为 group:
   - `has_conflict`:同组内 fingerprint 去重后数量 > 1(无 fingerprint 时按 1 处理)。

输出:`OnboardingPlan`(`total_tools_scanned/total_skills_found/groups`)。

### 6.3 Content Hash(目录指纹)

文件:`src-tauri/src/core/content_hash.rs`

实现要点:

- WalkDir 遍历目录(不 follow links)。
- 忽略:`.git`、`.DS_Store`、`Thumbs.db`、`.gitignore`(按名称)。
- 哈希包含相对路径 + 文件内容。

### 6.4 Sync Engine(混合同步)

文件:`src-tauri/src/core/sync_engine.rs`

核心策略:

- 目标不存在:
  1) 尝试 symlink(Unix;Windows 尝试 symlink_dir)
  2) Windows 额外尝试 junction(需要 `junction` crate)
  3) 最后回退 copy(递归复制)
- 目标已存在:
  - 若目标是指向 source 的同一个链接:视为已同步(幂等)。
  - 否则:
    - `overwrite=false`:报错 `target already exists`
    - `overwrite=true`:先删除目标目录,再按正常流程同步

设计取舍:

- 将“是否覆盖”作为显式参数,默认不覆盖,避免破坏用户既有目录。
- 通过 “先 staging、后 swap” 的更新策略,降低更新时半成品状态风险(见 installer 的 update)。

### 6.5 Installer(导入/更新)

文件:`src-tauri/src/core/installer.rs`

#### 本地导入(`install_local_skill`)

- 将 source 目录递归复制到 `central_repo/<name>`。
- 生成 `SkillRecord` 入库(`source_type=local`,`source_ref=source_path`)。
- 若中心目录已存在:报错 `skill already exists in central repo`(前端会映射为友好提示)。

#### Git 导入(`install_git_skill`)

- 解析 GitHub URL(支持 repo root、`.git`、`/tree/<branch>/<path>`、`/blob/<branch>/<path>`)。
- clone 到缓存临时目录(优先系统 `git` CLI,失败回退 libgit2),标记 `.skills-hub-git-temp`。
- 复制目标目录到中心仓库:
  - folder URL:复制 subpath
  - repo root URL:若检测到 `skills/` 下存在 >=2 个 `SKILL.md`,抛出 `MULTI_SKILLS|...` 引导用户改用 folder URL 或走候选选择流程
- 删除临时目录(best-effort)
- 入库 `source_type=git`、`source_ref=原始 URL`、`source_revision=HEAD`

#### Multi-skill 仓库候选(`list_git_skills` / `install_git_skill_from_selection`)

- `list_git_skills`:
  - root-level `SKILL.md` -> candidate `"."`
  - 扫描 `skills/*`、`skills/.curated/*`、`skills/.experimental/*`、`skills/.system/*`
  - 解析 `SKILL.md` 的 YAML front matter 获取 `name/description`(若存在)
- `install_git_skill_from_selection`:
  - clone -> copy -> 入库(类似 git 导入)
  - display name 默认取 subpath 末段或 repo 名

#### 更新(`update_managed_skill_from_source`)

- 根据 `skills.source_type` 重新构建新内容到 sibling staging dir:`.skills-hub-update-<uuid>`
- swap:删除旧中心目录 -> rename staging(跨盘 rename 失败则 copy fallback)
- 更新 `skills.updated_at/content_hash/source_revision` 等
- 若 `skill_targets.mode == "copy"`:对这些 target 执行 overwrite 同步,让工具目录内容跟随更新(symlink/junction 自动生效无需处理)

### 6.6 Git Fetcher(拉取策略)

文件:`src-tauri/src/core/git_fetcher.rs`

策略:

- 优先使用系统 `git` CLI(更符合用户本机网络/代理/证书/Keychain 配置)。
- CLI 失败再回退 libgit2(保持在无 git 环境仍可用)。

### 6.7 GitHub Search(仓库搜索)

文件:`src-tauri/src/core/github_search.rs`

- 调用 GitHub Search API:`https://api.github.com/search/repositories`
- `reqwest::blocking`,设置 `User-Agent: skills-hub`
- 返回 `RepoSummary`(full_name/html_url/description/stars/updated_at/clone_url)
- 备注:当前前端搜索 tab 为 disabled;可作为后续启用点。

### 6.8 Temp Cleanup(临时目录清理)

文件:`src-tauri/src/core/temp_cleanup.rs`

- 仅清理满足三重条件的目录:
  1) 位于 app_cache_dir
  2) 名称前缀 `skills-hub-git-`
  3) 含 marker 文件 `.skills-hub-git-temp`
- 并要求目录 `modified` 时间超过 max_age(目前 24h)。

## 7. Commands(前后端接口契约)

文件:`src-tauri/src/commands/mod.rs`

### 7.1 调用方式

前端通过 `@tauri-apps/api/core` 的 `invoke(command, args)` 调用;耗时操作统一在后端 `spawn_blocking`。

### 7.2 主要 commands 列表

- `get_central_repo_path() -> string`
- `set_central_repo_path(path: string) -> string`
- `get_tool_status() -> { tools[], installed[], newly_installed[] }`
- `get_onboarding_plan() -> OnboardingPlan`
- `get_managed_skills() -> ManagedSkill[]`
- `install_local(sourcePath: string, name?: string) -> InstallResultDto`
- `install_git(repoUrl: string, name?: string) -> InstallResultDto`
- `list_git_skills_cmd(repoUrl: string) -> GitSkillCandidate[]`
- `install_git_selection(repoUrl: string, subpath: string, name?: string) -> InstallResultDto`
- `import_existing_skill(sourcePath: string, name?: string) -> InstallResultDto`(当前与 `install_local` 等价)
- `sync_skill_dir(source_path: string, target_path: string) -> { mode_used, target_path }`(底层工具)
- `sync_skill_to_tool(sourcePath: string, skillId: string, tool: string, name: string, overwrite?: boolean) -> { mode_used, target_path }`
- `unsync_skill_from_tool(skillId: string, tool: string) -> void`
- `update_managed_skill(skillId: string) -> { skill_id, name, content_hash?, source_revision?, updated_targets[] }`
- `delete_managed_skill(skillId: string) -> void`
- `search_github(query: string, limit?: number) -> RepoSummary[]`

### 7.3 错误契约与前端分流

后端对 `anyhow::Error` 进行格式化,并保留以下前缀供前端识别:

- `MULTI_SKILLS|...`:仓库包含多个 skill,需要走候选选择或提供 folder URL。
- `TARGET_EXISTS|<path>`:目标目录存在且未覆盖,前端提示用户清理/取消勾选。
- `TOOL_NOT_INSTALLED|<tool>`:工具未安装。

此外对 GitHub clone 失败做了启发式中文提示(TLS/鉴权/DNS/超时等)。

## 8. 前端 UI 与交互设计

### 8.1 页面结构(当前为单页 Dashboard)

文件:`src/App.tsx` + `src/components/skills/*`

主要区域:

- Header:品牌、语言切换、设置、添加 Skill
- FilterBar:排序(updated/name)、搜索、刷新
- Discovered Banner:扫描到可导入 skills 时显示 “Review & Import”
- Skills List:卡片列表(每个 skill 显示来源、更新时间、工具 pills、更新/删除按钮)
- Modals:
  - AddSkillModal(local/git 两个 tab;选择 sync targets)
  - ImportModal(Onboarding plan 的 group/variant 选择与导入)
  - GitPickModal(multi-skill 仓库候选选择)
  - SettingsModal(语言、主题、中心仓库路径)
  - DeleteModal(删除确认)
  - NewToolsModal(新安装工具提示是否 sync all)
  - LoadingOverlay(耗时操作遮罩与提示)

### 8.2 i18n 与主题

- i18n:`react-i18next`,资源在 `src/i18n/resources.ts`,默认 `en`,可切换 `zh`。
- 主题:`src/index.css` 定义 CSS variables,`src/App.tsx` 使用 localStorage 保存 `skills-theme`(system/light/dark),并同步 `documentElement.dataset.theme`。

### 8.3 核心用户路径(前端侧)

#### 启动加载

1. 若运行在 Tauri 环境:
   - 拉取 `get_central_repo_path`(展示在设置)
   - 拉取 `get_tool_status`(工具安装状态 + newly installed 检测)
   - 拉取 `get_onboarding_plan`(用于 discovered banner)
   - 拉取 `get_managed_skills`(托管列表)

```mermaid
sequenceDiagram
  autonumber
  participant UI as Frontend (App.tsx)
  participant CMD as Tauri commands
  participant CORE as core/*
  participant DB as SQLite
  participant FS as FileSystem

  UI->>CMD: get_central_repo_path()
  CMD->>CORE: resolve + ensure_central_repo
  CORE->>DB: settings.get(central_repo_path)
  CORE->>FS: create_dir_all(~/.skillshub)
  CMD-->>UI: path

  UI->>CMD: get_tool_status()
  CMD->>CORE: default_tool_adapters + is_tool_installed
  CORE->>FS: exists(~/.cursor 等)
  CORE->>DB: settings.get/set(installed_tools_v1)
  CMD-->>UI: installed + newly_installed

  UI->>CMD: get_onboarding_plan()
  CMD->>CORE: build_onboarding_plan()
  CORE->>FS: scan_tool_dir + hash_dir
  CMD-->>UI: OnboardingPlan

  UI->>CMD: get_managed_skills()
  CMD->>DB: list_skills + list_skill_targets
  CMD-->>UI: ManagedSkill[]
```

#### 导入已发现 Skills(Review & Import)

1. 打开 ImportModal(若 plan 为空先调用 `get_onboarding_plan`)
2. 按 group 勾选要导入的技能
3. 若 `has_conflict`,必须在 variants 中选择一个来源路径
4. 执行导入:
   - 对每个选中 group:调用 `import_existing_skill(sourcePath=<chosen variant path>, name=<group name>)`
   - 再对“已安装且勾选的工具”逐个调用 `sync_skill_to_tool`
   - `overwrite` 策略:若同步回“来源工具”则 `overwrite=true`(接管);否则默认不覆盖

```mermaid
sequenceDiagram
  autonumber
  participant UI as ImportModal
  participant CMD as commands
  participant INST as installer
  participant SYNC as sync_engine
  participant DB as SkillStore(SQLite)
  participant FS as FileSystem

  UI->>CMD: import_existing_skill(sourcePath, name)
  CMD->>INST: install_local_skill()
  INST->>FS: copy sourcePath -> ~/.skillshub/<name>
  INST->>DB: upsert skills row
  CMD-->>UI: {skill_id, central_path, name}

  loop 对每个已选工具
    UI->>CMD: sync_skill_to_tool(sourcePath=central_path, tool, name, overwrite?)
    CMD->>SYNC: sync_dir_hybrid_with_overwrite()
    alt 目标不存在/可覆盖
      SYNC->>FS: symlink/junction/copy -> tool skills dir
      CMD->>DB: upsert skill_targets
      CMD-->>UI: {mode_used, target_path}
    else 目标存在且不覆盖
      CMD-->>UI: TARGET_EXISTS|<path>
    end
  end
```

#### 添加 Skill(Local/Git)

- local:先 `list_local_skills_cmd` 扫描目录(规则与 Git 一致),
  - 多个候选则弹出选择列表(无效候选置灰并标注原因)
  - 单个有效候选则 `install_local_selection` -> 对选中工具逐个 `sync_skill_to_tool`
- git:
  - folder URL:直接 `install_git` -> 同步
  - repo root URL:`list_git_skills_cmd` -> GitPickModal 选择 -> `install_git_selection` -> 同步

#### 切换工具生效(Tool Pills)

- 未生效 -> 生效:`sync_skill_to_tool(sourcePath=central_path, ...)`
- 已生效 -> 取消:`unsync_skill_from_tool(skillId, tool)`

#### 更新 & 删除

- 更新:`update_managed_skill(skillId)`(后端会对 copy targets 回灌更新)
- 删除:`delete_managed_skill(skillId)`(先清理工具目录映射,再删除中心目录与 DB)

```mermaid
flowchart TB
  A[更新 update_managed_skill] --> B{source_type}
  B -->|git| C[clone 到 cache temp\nskills-hub-git-*]
  B -->|local| D[读取 source_ref 目录]
  C --> E[copy 到 staging dir\n.skills-hub-update-*]
  D --> E
  E --> F[swap: 删除旧 central_dir\nrename/copy 回 central_path]
  F --> G[更新 skills.updated_at/hash/revision]
  G --> H{targets.mode == copy ?}
  H -->|是| I[overwrite 同步到 target_path]
  H -->|否| J[symlink/junction 自动生效]
```

## 9. 关键一致性与安全策略

### 9.1 “默认不破坏用户环境”

- 同步默认 `overwrite=false`,目标存在即失败并给出 `TARGET_EXISTS|...`。
- 仅在明确需要“接管”的场景,前端才传 `overwrite=true`(当前:导入 discovered skill 且同步回来源工具)。

### 9.2 删除与清理的边界

- `delete_managed_skill` 仅清理 DB 中记录过的 `skill_targets.target_path`,不会全盘扫描/删除工具目录。
- Git 临时目录清理限定 prefix + marker + age gate,降低误删风险。

### 9.3 权限与跨平台差异

- Windows 上 symlink 可能受权限/策略限制:引擎会尝试 junction,最后回退 copy。
- 复制模式的 targets 需要在更新时显式回灌,否则工具目录可能滞后;后端已实现该传播逻辑。

## 10. 性能与稳定性

- 后端耗时操作使用 `spawn_blocking`,避免阻塞 UI。
- 前端 LoadingOverlay 在长操作期间提供提示(clone/IO 10–60s)。
- Git 拉取优先系统 git,提高 macOS 网络/证书兼容性。
- 错误信息:
  - 尽量包含 root cause
  - 对 clone “临时目录路径”做脱敏(减少噪音)
  - 对 GitHub/TLS/鉴权等提供可行动提示

## 11. 测试与验证建议(仓库当前缺少自动化测试时的落地方案)

> 当前仓库未显式提供 Rust/前端测试用例。建议按模块逐步补齐:

- Rust(core):
  - `content_hash`:忽略文件名/顺序稳定性
  - `parse_github_url`:覆盖 repo/tree/blob/.git 组合
  - `sync_engine`:用临时目录验证 overwrite/幂等行为(平台差异可通过条件编译分组)
- 前端:
  - `App` 的业务逻辑建议逐步下沉到 hooks(便于单测)
  - Modal 表单校验与错误映射(`TARGET_EXISTS|` 等)

## 12. 现状梳理与后续路线图(建议)

### 12.1 已完成(与实现一致)

- 多工具 adapter 支持与安装检测
- Onboarding 扫描(冲突检测 + link 展示)
- local/git 导入与 multi-skill 候选选择
- 混合同步(symlink/junction/copy)
- 更新(copy targets 回灌)
- 新安装工具提示
- 中心仓库路径设置与迁移

### 12.2 后续增强方向(按价值/风险)

1. **启用 GitHub 搜索 UI**:对接 `search_github`,并支持一键安装候选仓库。
2. **扫描结果落库**:使用 `discovered_skills` 表持久化“发现但未导入”的技能,支持忽略/标记与增量刷新。
3. **Onboarding gating**:引入 `settings.onboarding_completed`,仅在首次启动/用户触发时弹出导入引导,避免每次都显示 discovered banner。
4. **更强冲突策略**:支持 `name@variant` 的版本并存(需要 UI 显式展示与命名规范)。
5. **维护任务**:提供“清理失效 targets / 修复 broken link / 重新同步所有 copy targets”入口。


================================================
FILE: docs/releases/v0.3.0/plan-bug-fixes.md
================================================
# Bug 修复计划

来源:https://github.com/qufei1993/skills-hub/issues

---

## Bug 1:Git 安装时 skill 名称为 "skills" 导致路径重复(#28)

**Issue**: https://github.com/qufei1993/skills-hub/issues/28
**严重程度**: P0
**状态**: ✅ 已修复(commit 69ab806)

### 问题描述

通过 Git URL 安装 skill 时,如果 URL 指向名为 `skills` 的子目录(如 `https://github.com/xxx/repo/tree/main/skills`),`install_git_skill` 会从 subpath 推导 name 为 `"skills"`,导致同步到工具时路径变成 `~/.claude/skills/skills/`。

### 根因

`installer.rs:94-104` 的 name 推导逻辑只看 subpath/URL,不读 SKILL.md 的 name 字段:

```rust
let name = name.unwrap_or_else(|| {
    if let Some(subpath) = &parsed.subpath {
        subpath.rsplit('/').next()  // ← subpath 是 "skills" 时,name 就是 "skills"
```

而本地导入走 `install_local_skill_from_selection`,会先从 SKILL.md 读 name,所以不受影响。

### 复现步骤

1. 在 Skills Hub 中选择「Git 仓库」
2. 输入 `https://github.com/anthropics/skills/tree/main/skills`
3. 不填显示名称,直接安装
4. 同步到 Claude Code 后,路径变成 `~/.claude/skills/skills/`

### 修复方案

修改 `installer.rs` 的 `install_git_skill`,在 name 推导后、写入 central_path 之前,尝试从已下载内容的 SKILL.md 读取 name 覆盖。同时增加保底校验:如果 name 仍为 `"skills"`,报错要求用户手动指定名称。

### 涉及文件

- `src-tauri/src/core/installer.rs` — `install_git_skill` 函数 name 推导逻辑

---

## Bug 2:GitHub API 限流错误未提示重置时间

**Issue**: 使用中发现(关联 #28 复现过程)
**严重程度**: P2
**状态**: ✅ 已修复

### 问题描述

当 GitHub API 返回 403(速率限制)时,错误提示只显示"请稍后再试",未告诉用户具体的重置时间。GitHub 响应头中包含 `x-ratelimit-reset` 字段(Unix 时间戳),应提取并展示。

### 根因

`github_download.rs:70` 调用 `.error_for_status()` 直接将 403 转为通用错误,丢弃了响应头信息。`installer.rs:148-149` 捕获 403 时也只返回固定文案。

### 修复方案

在 `github_download.rs` 中,不使用 `.error_for_status()`,而是手动检查 status code。当遇到 403 时,从响应头提取 `x-ratelimit-reset` 和 `x-ratelimit-remaining`,将重置时间格式化为本地时间后包含在错误消息中。

示例错误消息:`"GitHub API 访问被拒绝(触发了频率限制)。将于 18:44 重置,请届时再试。"`

### 涉及文件

- `src-tauri/src/core/github_download.rs` — HTTP 请求错误处理
- `src-tauri/src/core/installer.rs` — 403 错误消息构造

---

## 改进:设置页增加 GitHub Token 配置

**严重程度**: P1
**状态**: ✅ 已实现(commit d2c1cc0)

### 问题描述

当前所有 GitHub API 请求均为未认证(60 次/小时/IP),安装 skill 时递归下载目录会快速耗尽配额。用户无法配置 Token 来提升限额。

### 方案

1. **设置页 UI**:增加一个可选的 GitHub Token 输入框(密码类型,支持显示/隐藏),存入 `settings` 表(key: `github_token`)
2. **后端传递**:`github_download.rs` 和 `github_search.rs` 发请求时,从 SkillStore 读取 token,如果存在则加上 `Authorization: Bearer <token>` 请求头
3. **限额提升**:认证后 GitHub API 限额从 60 → 5000 次/小时(按账号计算)
4. **安全**:Token 仅存本地 SQLite,不上传不同步。设置页提示用户生成 fine-grained PAT,只需 `public_repo` 读取权限

### 涉及文件

- `src-tauri/src/core/github_download.rs` — 请求时携带 token
- `src-tauri/src/core/github_search.rs` — 请求时携带 token
- `src-tauri/src/core/skill_store.rs` — 读取 `github_token` 设置
- `src/App.tsx` — 设置弹窗增加 GitHub Token 输入
- `src/i18n/resources.ts` — 新增中英文翻译

---

## Bug 3:Windows 拒绝访问 OS error 5(#20)

**Issue**: https://github.com/qufei1993/skills-hub/issues/20
**严重程度**: P0
**状态**: ✅ 已修复(commit 93d9aca)

### 问题描述

Windows 用户点击 AI-IDE 同步选项时报 OS error 5(权限不足),即使对应工具未安装。

### 可能原因

1. Windows 上创建 symlink 需要管理员权限或开发者模式,`sync_engine.rs` 的 fallback(symlink → junction → copy)可能在某些环境下全部失败
2. 尝试同步到未安装工具的目录时,`is_tool_installed` 检查可能误判(检测目录存在但无写入权限)

### 涉及文件

- `src-tauri/src/core/sync_engine.rs` — symlink/junction/copy fallback 逻辑
- `src-tauri/src/core/tool_adapters/mod.rs` — `is_tool_installed` 检测逻辑
- `src-tauri/src/commands/mod.rs` — `sync_skill_to_tool` 错误处理

---

## Bug 4:Skill 扫描逻辑对部分目录结构失效(#18 + #8)

**Issue**: https://github.com/qufei1993/skills-hub/issues/18 / https://github.com/qufei1993/skills-hub/issues/8
**严重程度**: P1
**状态**: ✅ 已修复

### 问题描述

- #18:某些 git 仓库目录结构无法被正确识别为 skill
- #8:skill 显示在仓库中但实际不存在;发现的 skill 无法导入

### 根因

**#18**:`list_git_skills` 和 `install_git_skill` 的多技能检测只扫描 `skills/` 目录下的子目录。当仓库把 skill 直接放在根目录的子文件夹(如 `repo/my-skill/SKILL.md`,不套 `skills/` 父目录)时,扫描结果为空,用户看到"该仓库中没有 Skills"。

示例仓库:`axtonliu/axton-obsidian-visual-skills`,结构为 `repo/excalidraw-diagram/SKILL.md`、`repo/mermaid-visualizer/SKILL.md` 等,无 `skills/` 目录。

**#8**:`scan_tool_dir`(onboarding 扫描)把工具 skills 目录下的**每个子目录**都当成 skill(不检查 SKILL.md),导致不含 SKILL.md 的目录也被"发现",但导入时又因缺少 SKILL.md 失败。

### 修复方案

1. **#18**:在 `list_git_skills` 中增加扫描仓库根目录直接子目录中的 SKILL.md;在 `install_git_skill` 的多技能检测中同样覆盖根目录子目录
2. **#8**:`scan_tool_dir` 保持现有行为(不强制要求 SKILL.md),但前端导入时已有校验。或者在 `scan_tool_dir` 中区分"有 SKILL.md"和"无 SKILL.md"的目录

### 涉及文件

- `src-tauri/src/core/installer.rs` — `list_git_skills` 和 `install_git_skill` 的扫描范围
- `src-tauri/src/core/tool_adapters/mod.rs` — `scan_tool_dir` 扫描逻辑

---

## Bug 5:Skill 名称冲突无法安装(#12)

**Issue**: https://github.com/qufei1993/skills-hub/issues/12
**严重程度**: P1
**状态**: ✅ 已关闭(PR #30 合并,UI 已有「显示名称」输入框可手动指定别名,冲突时提示用户重命名)

### 涉及文件

- `src-tauri/src/core/installer.rs` — 安装时 name 冲突处理

---

## Bug 6:新增 skill 未被自动扫描(#19)

**Issue**: https://github.com/qufei1993/skills-hub/issues/19
**严重程度**: P2
**状态**: ✅ 已关闭(非 bug,预期行为)

### 调查结论

`~/.skillshub/` 是 Skills Hub 的内部存储,外部工具不应直接写入。实际上 OpenCode 创建的 skill 会落在 `~/.config/opencode/skills/` 下(普通目录),不会进入 `~/.skillshub/`。用户误以为写入了 skillshub 目录。

如需将外部工具中新建的 skill 纳入 Skills Hub 管理,可通过"导入已有 Skill"功能手动操作。

---

## Bug 7:不支持 .claude/skills/ 目录格式的仓库(#27)

**Issue**: https://github.com/qufei1993/skills-hub/issues/27
**严重程度**: P1
**状态**: ✅ 已修复(PR #31 合并)

### 问题描述

使用 `.claude-plugin/plugin.json` + `.claude/skills/` 目录结构的仓库(如 `nextlevelbuilder/ui-ux-pro-max-skill`)无法被 Skills Hub 识别和安装,因为扫描逻辑只查找 `SKILL.md` 文件。

### 根因

`list_git_skills`、`count_skills_in_repo`、`list_local_skills` 只扫描 `skills/`、`skills/.curated/` 等目录,不扫描 `.claude/skills/`。且只认 `SKILL.md` 为有效标记,`.claude/skills/` 下的目录即使内容完整也被忽略。

### 修复方案

1. 新增 `.claude/skills/` 为扫描路径(与 `skills/` 并列)
2. `.claude/skills/` 下的子目录即使没有 `SKILL.md` 也视为有效 skill
3. 无 `SKILL.md` 时用文件夹名做 name,从 `.claude-plugin/plugin.json` 读取 description

### 涉及文件

- `src-tauri/src/core/installer.rs` — `SKILL_SCAN_BASES` 常量、`is_skill_dir`、`is_claude_skill_dir`、`read_plugin_description`、`extract_skill_info` 辅助函数

---

## Bug 8:界面上没有 OpenClaw 的同步(#29)

**Issue**: https://github.com/qufei1993/skills-hub/issues/29
**严重程度**: P2
**状态**: ✅ 已关闭(PR #26 已修复代码,文档已补充更新)

### 问题描述

用户界面上找不到 OpenClaw 的同步选项。

### 根因

OpenClaw 更名后将配置目录从 `.moltbot/` 迁移到 `.openclaw/`,代码中的 `tool_adapters` 仍指向旧路径。

### 修复内容

1. PR #26(社区贡献)修复了代码:OpenClaw 路径 `.moltbot/skills` → `.openclaw/skills`,原 `.moltbot` 拆分为独立的 MoltBot 工具
2. 补充更新了 `README.md` 和 `docs/README.zh.md` 中的工具列表(OpenClaw 路径 + MoltBot 新增行)
3. 补充了 `src/i18n/resources.ts` 中缺少的 `moltbot: 'MoltBot'` 翻译

### 涉及文件

- `src-tauri/src/core/tool_adapters/mod.rs` — OpenClaw 路径更新 + MoltBot 新增(PR #26)
- `README.md` — 工具列表更新
- `docs/README.zh.md` — 工具列表更新
- `src/i18n/resources.ts` — MoltBot 翻译新增


================================================
FILE: docs/releases/v0.3.0/plan-explore-page-redesign.md
================================================
# 需求:Explore 页面独立化 + My Skills 列表优化

## 背景

当前"添加 Skill"的交互流程存在问题:探索、本地添加、Git 添加三个功能挤在一个弹窗的三个 Tab 里。用户在探索 Tab 点击一个 skill 后,只是把 URL 填入 Git Tab 并跳转,还需手动确认并点击安装,流程冗长(5 步)。

## 设计稿

`docs/skills_hub_v2_design.html` — 包含 4 个屏幕的完整交互设计。

## 改动概览

### 一、导航结构调整

**现状**:单页面 + 弹窗内 3 个 Tab(探索/本地/Git)
**目标**:顶部导航 2 个页面级 Tab + 弹窗仅保留手动添加

- App Header 内新增 **My Skills** / **Explore** 两个导航 Tab
- 点击切换页面视图(非路由,纯前端状态切换)
- 默认展示 My Skills 页

### 二、Explore 页(新页面)

将探索功能从弹窗提升为独立页面,作为"获取新 Skill 的唯一入口"。

#### 布局
- 顶部:搜索栏 + **Manual** 按钮(触发手动添加弹窗)
- 搜索栏下方:提示文字 "Data from clawhub.ai · Have a Git URL or local path? Click Manual to add directly"
- 内容区:双列卡片网格

#### Explore 卡片
每张卡片展示:
- Skill 名称
- 作者/来源(repo 路径)
- 描述(最多 2 行截断)
- 下载量 / Stars
- 兼容工具小标签(折叠显示,如 `Cursor` `Claude` `+5`)
- **Install 按钮**(右上角,一键安装)
- 已安装的 Skill 显示为绿色 **Installed** 状态(禁用按钮)

#### 一键安装流程
- 点击 Install → 自动使用所有已检测到的工具作为同步目标 → 安装并同步
- 安装成功后底部 toast 提示:"xxx installed and synced to N tools"
- 安装完成后按钮状态变为 Installed

#### 搜索态
- 输入 ≥ 2 字符触发搜索
- 结果分为 "Featured Matches" 和 "Online Results" 两个区域
- 搜索关键词高亮

### 三、Manual Add 弹窗(精简)

- 从 Explore 页的 Manual 按钮触发
- **移除探索 Tab**,仅保留 Local Directory / Git Repository 两个 Tab
- 其余逻辑不变(工具选择器、安装流程)

### 四、My Skills 页(列表优化)

#### 移除
- 移除 Add 按钮(添加功能统一收口到 Explore 页)

#### 卡片增加 description
- 每张卡片新增描述文本,显示在名称下方,最多 2 行截断
- 信息层次:名称 → 描述 → 来源+时间 → 工具徽章

#### 工具徽章优化
- **只显示已同步的工具**(绿色带圆点),不再显示未同步的灰色徽章
- 超过 5 个折叠为 `+N more`
- 减少视觉噪音,让每张卡片高度一致

## 后端改动

### 新增 description 字段

当前 `ManagedSkillDto` 没有 description 字段,需要:

1. 从 Skill 目录的 `SKILL.md` frontmatter 中解析 `description` 字段
2. 在安装时提取并存入数据库(`skills` 表新增 `description` 列)
3. `ManagedSkillDto` 新增 `description: Option<String>` 字段返回给前端

### 涉及文件
- `src-tauri/src/core/skill_store.rs` — 表结构迁移,新增 description 列
- `src-tauri/src/core/installer.rs` — 安装时解析 SKILL.md 提取 description
- `src-tauri/src/commands/mod.rs` — DTO 新增字段

## 前端改动

### 涉及文件
- `src/App.tsx` — 新增页面级 Tab 状态,拆分 Explore 视图逻辑
- `src/App.css` — Explore 页样式、卡片优化样式
- `src/components/skills/types.ts` — DTO 同步新增 description 字段
- `src/components/skills/modals/AddSkillModal.tsx` — 移除探索 Tab,仅保留 Local/Git
- `src/i18n/resources.ts` — 新增/调整翻译 key(My Skills / Explore 导航等)

### 可能新增文件
- `src/components/skills/ExplorePage.tsx` — Explore 页面组件
- `src/components/skills/ExploreCard.tsx` — Explore 卡片组件

## 实施顺序建议

1. **后端**:description 字段(表迁移 + SKILL.md 解析 + DTO)
2. **前端**:导航 Tab 切换 + My Skills 列表优化(description 展示 + 工具徽章折叠)
3. **前端**:Explore 页面(搜索 + 卡片网格 + 一键安装)
4. **前端**:Manual Add 弹窗精简(移除探索 Tab,改为从 Explore 页触发)
5. **联调 & 测试**


================================================
FILE: docs/releases/v0.3.0/plan-featured-skills.md
================================================
# 需求一:精选技能推荐列表 — 实施计划

## Context

用户打开"添加技能"时,只有手动输入本地路径或 Git URL,缺乏发现能力。需要在 AddSkillModal 中新增"探索"标签页,展示由 CI 预生成的热门技能列表,用户点击后自动走现有 Git 安装流程。

数据源:ClawHub API (`clawhub.ai/api/v1/skills`),由 GitHub Actions 每日拉取生成 `featured-skills.json` 提交到仓库。应用运行时从 GitHub raw URL 获取该 JSON(带本地缓存兜底),避免直接依赖 ClawHub API。

## 关键发现

- AddSkillModal 已有一个 **disabled 的搜索标签按钮**(第 88-90 行),可直接改造为"探索"
- `addModalTab` 类型当前为 `'local' | 'git'`,需扩展为三值联合
- 后端已有 `reqwest::blocking::Client` + `github_search.rs` 的 HTTP 模式可复用
- `SkillStore` 已有 `get_setting` / `set_setting` 可用于缓存
- `parse_github_url` 已支持 `https://github.com/owner/repo/tree/branch/path` 格式
- 安装 URL 格式:`https://github.com/openclaw/skills/tree/main/skills/{username}/{slug}`

## 步骤 1:CI — GitHub Actions 工作流 + 拉取脚本

### 新建 `.github/workflows/update-featured-skills.yml`

- 每日 UTC 0:00 定时运行 + 支持手动触发
- 执行 Node.js 脚本 `scripts/fetch-featured-skills.mjs`
- 若 JSON 有变化则自动提交

### 新建 `scripts/fetch-featured-skills.mjs`

逻辑:
1. 调用 `GET https://clawhub.ai/api/v1/skills?sort=downloads&limit=100`
2. 调用 GitHub API 获取 `openclaw/skills` 仓库 `skills/` 目录结构(用于匹配 slug → 实际路径)
3. 对每个 ClawHub 技能,在 `openclaw/skills` 目录中按 slug 匹配找到 `{username}/{slug}` 路径
4. 生成 `featured-skills.json`,结构:

```json
{
  "updated_at": "2026-03-13T00:00:00Z",
  "skills": [
    {
      "slug": "self-improving-agent",
      "name": "self-improving-agent",
      "summary": "Captures learnings, errors...",
      "downloads": 197815,
      "stars": 1934,
      "source_url": "https://github.com/openclaw/skills/tree/main/skills/username/self-improving-agent"
    }
  ]
}
```

5. 未匹配到 GitHub 路径的技能,`source_url` 留空,前端不显示安装按钮

### 同时先手动运行一次脚本,生成初始 `featured-skills.json` 提交到仓库根目录

## 步骤 2:后端 — 新建 `core/featured_skills.rs`

参考 `github_search.rs` (src-tauri/src/core/github_search.rs) 模式。

```rust
// 数据结构
pub struct FeaturedSkillsData { updated_at: String, skills: Vec<FeaturedSkill> }
pub struct FeaturedSkill { slug, name, summary, downloads: u64, stars: u64, source_url: String }

// 核心函数
pub fn fetch_featured_skills(store: &SkillStore) -> Result<Vec<FeaturedSkill>>
```

逻辑:
1. `reqwest::blocking::Client` 请求 `https://raw.githubusercontent.com/{owner}/{repo}/main/featured-skills.json`
2. 成功 → 解析 JSON,缓存到 `store.set_setting("featured_skills_cache", &json_str)`
3. 失败 → 从 `store.get_setting("featured_skills_cache")` 读缓存
4. 都失败 → 返回空 Vec(优雅降级,不 bail)
5. 过滤掉 `source_url` 为空的条目

### 修改 `core/mod.rs`

添加 `pub mod featured_skills;`

## 步骤 3:后端 — 注册 Tauri 命令

### 修改 `src-tauri/src/commands/mod.rs`

新增 DTO 和命令:

```rust
#[derive(Debug, Serialize)]
pub struct FeaturedSkillDto {
    pub slug: String,
    pub name: String,
    pub summary: String,
    pub downloads: u64,
    pub stars: u64,
    pub source_url: String,
}

#[tauri::command]
pub async fn get_featured_skills(store: State<'_, SkillStore>) -> Result<Vec<FeaturedSkillDto>, String>
```

使用标准的 `spawn_blocking` + `format_anyhow_error` 模式。

### 修改 `src-tauri/src/lib.rs`

在 `generate_handler!` 中注册 `commands::get_featured_skills`。

## 步骤 4:前端 — 类型定义

### 修改 `src/components/skills/types.ts`

```typescript
export type FeaturedSkillDto = {
  slug: string
  name: string
  summary: string
  downloads: number
  stars: number
  source_url: string
}
```

## 步骤 5:前端 — App.tsx 状态管理

### 修改 `src/App.tsx`

1. **Tab 类型扩展**:`useState<'local' | 'git' | 'explore'>('explore')` — 默认打开探索标签
2. **新增状态**:
   - `const [featuredSkills, setFeaturedSkills] = useState<FeaturedSkillDto[]>([])`
   - `const [featuredLoading, setFeaturedLoading] = useState(false)`
   - `const [exploreFilter, setExploreFilter] = useState('')`
3. **新增 `loadFeaturedSkills`**:调用 `invoke('get_featured_skills')`,在 `handleOpenAdd` 中触发(仅首次或数据为空时)
4. **新增 `handleSelectFeaturedSkill(sourceUrl: string)`**:
   - `setGitUrl(sourceUrl)`
   - `setAddModalTab('git')` — 自动跳转到 Git 标签
5. **传递新 props 给 AddSkillModal**:`featuredSkills`, `featuredLoading`, `exploreFilter`, `onExploreFilterChange`, `onSelectFeaturedSkill`

## 步骤 6:前端 — 修改 AddSkillModal

### 修改 `src/components/skills/modals/AddSkillModal.tsx`

1. **Props 类型扩展**:添加 explore 相关的 6 个新 props
2. **启用探索标签**:替换第 88-90 行 disabled 按钮为可点击的 explore 标签
3. **三分支条件渲染**:
   - `addModalTab === 'explore'` → 探索内容
   - `addModalTab === 'local'` → 本地表单(现有)
   - `addModalTab === 'git'` → Git 表单(现有)
4. **探索标签内容**:
   - 筛选输入框(前端过滤 name + summary)
   - 可滚动列表(max-height ~400px)
   - 每项显示:name(粗体)、summary(截断)、downloads + stars 统计
   - 点击 → 调用 `onSelectFeaturedSkill(source_url)`
   - Loading / Empty 状态
5. **条件隐藏底部区域**:explore 标签时隐藏 "Install to tools" 复选框区域和 footer 按钮(用户在此标签只浏览,安装在 git 标签完成)

## 步骤 7:前端 — i18n 翻译

### 修改 `src/i18n/resources.ts`

```
EN:
  exploreTab: 'Explore'
  exploreFilterPlaceholder: 'Filter skills...'
  exploreEmpty: 'No featured skills available.'
  exploreLoading: 'Loading featured skills...'
  exploreError: 'Failed to load featured skills.'

ZH:
  exploreTab: '探索'
  exploreFilterPlaceholder: '筛选技能...'
  exploreEmpty: '暂无精选技能。'
  exploreLoading: '加载精选技能中...'
  exploreError: '加载精选技能失败。'
```

## 步骤 8:前端 — CSS 样式

### 修改 `src/App.css`

添加探索标签页样式:
- `.explore-filter` — 输入框
- `.explore-list` — 可滚动容器 (max-height: 400px, overflow-y: auto)
- `.explore-skill-item` — 单条技能行 (cursor: pointer, hover 高亮)
- `.explore-skill-name` — 名称
- `.explore-skill-summary` — 简介 (text-overflow: ellipsis)
- `.explore-skill-stats` — 统计数字
- `.explore-empty` / `.explore-loading` — 状态提示

## 步骤 9:后端测试

### 新建 `src-tauri/src/core/tests/featured_skills.rs`

使用 `mockito` mock HTTP:
- 测试正常 JSON 解析
- 测试 HTTP 失败时缓存 fallback
- 测试空/畸形 JSON 的优雅降级

## 修改文件清单

| 文件 | 操作 | 说明 |
|------|------|------|
| `.github/workflows/update-featured-skills.yml` | 新建 | CI 定时任务 |
| `scripts/fetch-featured-skills.mjs` | 新建 | 数据拉取脚本 |
| `featured-skills.json` | 新建 | CI 生成的精选列表 |
| `src-tauri/src/core/featured_skills.rs` | 新建 | 后端核心逻辑 |
| `src-tauri/src/core/mod.rs` | 修改 | 导出新模块 |
| `src-tauri/src/commands/mod.rs` | 修改 | 新增命令 + DTO |
| `src-tauri/src/lib.rs` | 修改 | 注册命令 |
| `src/components/skills/types.ts` | 修改 | 新增前端 DTO |
| `src/App.tsx` | 修改 | 状态 + 回调 + props |
| `src/components/skills/modals/AddSkillModal.tsx` | 修改 | UI 改造核心 |
| `src/i18n/resources.ts` | 修改 | 中英文翻译 |
| `src/App.css` | 修改 | 探索标签样式 |
| `src-tauri/src/core/tests/featured_skills.rs` | 新建 | 后端测试 |

## 验证方式

1. `npm run check` — lint + build + rust:clippy + rust:test 全部通过
2. `npm run tauri:dev` — 打开 AddSkillModal,默认显示"探索"标签
3. 确认列表加载正常(或网络失败时显示友好提示)
4. 输入关键词,确认筛选功能工作
5. 点击某个技能,确认自动跳转到 Git 标签并填充 URL
6. 在 Git 标签点击安装,确认走通完整安装流程


================================================
FILE: docs/releases/v0.3.0/plan-online-search.md
================================================
# 需求二:在线技能搜索与安装 — 实施计划(已完成)

## Context

需求一已实现"探索"标签页,展示精选技能列表。但精选列表覆盖有限,用户有明确需求时(如"找 React 相关技能"),需要实时搜索能力。搜索功能内嵌在探索标签页中:用户输入关键词 → 精选本地过滤秒出 + 在线搜索结果分区展示,清空搜索框 → 恢复精选列表。

数据源:`skills.sh/api/search?q={query}&limit=20`(无需认证,模糊搜索,最少 2 字符)。无 CORS,必须从 Rust 后端调用。

### API 返回字段

```json
{
  "id": "vercel-labs/agent-skills/vercel-react-best-practices",
  "skillId": "vercel-react-best-practices",
  "name": "vercel-react-best-practices",
  "installs": 205992,
  "source": "vercel-labs/agent-skills"
}
```

> **重要发现**:`id` 和 `name` 不能直接映射到仓库内的文件路径。例如 API `name: "json-render-react"` 对应仓库目录 `skills/react/`、SKILL.md `name: "react"`。skills.sh 平台对名称做了转换,与仓库实际 SKILL.md frontmatter name 不一致。

## 核心设计决策

### 方案:分区展示,精选 + 在线互补

输入框升级为双模式,两个数据源分区展示:
- 输入 < 2 字符:仅前端本地过滤精选列表(现有行为不变)
- 输入 >= 2 字符:上方显示精选列表本地过滤结果(秒出,有 summary),下方分割线后显示在线搜索结果(500ms 防抖,有 installs)
- 清空输入框:隐藏在线搜索区域,恢复完整精选列表

```
用户输入 "react"
  ↓ 立刻
  ┌─ 精选推荐(本地过滤) ──────────────┐
  │  vercel-react-best-practices        │  ← 有 summary
  │  vercel-react-native-skills         │
  └─────────────────────────────────────┘
  ↓ 500ms 后
  ┌─ 在线搜索 skills.sh ───────────────┐
  │  react-expert (203K installs)       │  ← 无 summary,有 installs
  │  react (57K installs)               │
  └─────────────────────────────────────┘
```

### 在线搜索去重

在线结果中与精选列表 name 重复的条目自动过滤(`useMemo` 按 name 去重)。

### 点击安装 — 多技能仓库自动匹配

点击在线搜索结果时,`source_url` 是仓库地址(`https://github.com/owner/repo`),不含子目录路径(因为 API 数据无法可靠映射到仓库文件路径)。安装流程:

1. 设置 `gitUrl` = 仓库地址,`autoSelectSkillName` = API 返回的 skill name
2. 用户点安装 → `list_git_skills_cmd` 克隆仓库列出所有候选 skill
3. **自动匹配策略**(三级回退):
   - 精确匹配:API name === SKILL.md name(如 `vercel-react-best-practices`)
   - 唯一包含匹配:API name 包含某个 SKILL.md name,且仅一个候选命中(如 `json-render-react` 包含 `react`)
   - 回退 picker:匹配失败或多个候选命中时,弹出选择弹窗让用户手动选
4. 单技能仓库:直接安装(现有逻辑不变)

## 实现步骤

### 步骤 1:后端 — `core/skills_search.rs`(新建)

参考 `github_search.rs` 模式。

```rust
#[derive(Debug, Deserialize)]
struct SkillsShResponse { skills: Vec<SkillsShItem> }
struct SkillsShItem { name: String, installs: u64, source: String }

pub struct OnlineSkillResult {
    pub name: String,
    pub installs: u64,
    pub source: String,        // "owner/repo"
    pub source_url: String,    // "https://github.com/owner/repo"
}

pub fn search_skills_online(query: &str, limit: usize) -> Result<Vec<OnlineSkillResult>>
// 内部函数 search_skills_online_inner(base_url, query, limit) 支持测试注入
```

`source_url` 由 `source` 字段拼接(`https://github.com/{source}`)。不使用 `id` 字段构造路径(无法映射到仓库实际目录结构)。

在 `core/mod.rs` 添加 `pub mod skills_search;`。

### 步骤 2:后端 — 注册 Tauri 命令

`commands/mod.rs` 新增:

```rust
#[derive(Debug, Serialize)]
pub struct OnlineSkillDto { name, installs, source, source_url }

impl From<OnlineSkillResult> for OnlineSkillDto { ... }

#[tauri::command]
pub async fn search_skills_online(query: String, limit: Option<u32>) -> Result<Vec<OnlineSkillDto>, String>
```

不需要 `State<SkillStore>`(纯 HTTP 调用)。在 `lib.rs` 的 `generate_handler!` 中注册。

### 步骤 3:前端 — 类型定义

`src/components/skills/types.ts` 新增:

```typescript
export type OnlineSkillDto = {
  name: string
  installs: number
  source: string      // "owner/repo"
  source_url: string  // "https://github.com/owner/repo"
}
```

### 步骤 4:前端 — App.tsx 状态与逻辑

1. **新增状态**:
   - `searchResults: OnlineSkillDto[]` — 在线搜索结果
   - `searchLoading: boolean` — 搜索加载中
   - `searchTimerRef: useRef` — 500ms 防抖 timer
   - `autoSelectSkillName: string | null` — 从在线搜索点击时记录的目标 skill 名称

2. **`handleExploreFilterChange`**:
   - 设置 `exploreFilter`(驱动精选列表本地过滤,秒出)
   - < 2 字符 → 清除 timer + 清空搜索结果
   - >= 2 字符 → 500ms 防抖后调用 `invoke('search_skills_online', { query, limit: 20 })`

3. **`handleSelectSearchResult(sourceUrl, skillName)`**:
   - `setGitUrl(sourceUrl)` + `setAutoSelectSkillName(skillName)` + 切换到 git tab

4. **`handleCreateGit` 中多技能分支增加自动匹配**:
   - 当 `autoSelectSkillName` 存在且 `candidates.length > 1` 时,按三级回退策略自动匹配
   - 匹配成功 → 直接 `install_git_selection` 安装
   - 匹配失败 → 回退到 picker 弹窗

### 步骤 5:前端 — AddSkillModal 分区布局

Props 新增:`searchResults`, `searchLoading`, `onSelectSearchResult(sourceUrl, skillName)`

```
{/* 区域 1:精选推荐(始终显示,本地过滤) */}
{isSearchActive && <SectionTitle>精选推荐</SectionTitle>}
<explore-list>精选过滤结果</explore-list>

{/* 区域 2:在线搜索(仅 >= 2 字符时显示) */}
{isSearchActive && <>
  <SectionTitle>在线搜索</SectionTitle>
  {searchLoading ? <Loading /> : deduplicatedResults.map(skill => (
    <SkillItem name={skill.name} source={skill.source} installs={skill.installs}
               onClick={() => onSelectSearchResult(skill.source_url, skill.name)} />
  ))}
</>}

{/* 全局空状态 */}
{无精选 && 无搜索 && <Empty />}
```

去重:`useMemo` 从 `searchResults` 中过滤掉 name 已存在于 `filteredSkills` 的条目。

### 步骤 6:i18n 翻译

```
EN:
  exploreFilterPlaceholder: 'Filter or search skills online...'
  exploreFeaturedTitle: 'Featured'
  exploreOnlineTitle: 'Online Results'
  searchLoading: 'Searching skills.sh...'
  searchEmpty: 'No additional results found.'
  searchError: 'Online search failed.'

ZH:
  exploreFilterPlaceholder: '筛选精选或在线搜索技能...'
  exploreFeaturedTitle: '精选推荐'
  exploreOnlineTitle: '在线搜索'
  searchLoading: '正在搜索 skills.sh...'
  searchEmpty: '未找到更多结果。'
  searchError: '在线搜索失败。'
```

### 步骤 7:CSS 样式

- `.explore-section-title` — 分区标题(小字灰色,带上边框分隔,uppercase)
- `.explore-skill-source` — source 字段(灰色小字 12px)
- 复用现有 `.explore-skill-item` / `.explore-list` 样式

### 步骤 8:后端测试

`src-tauri/src/core/tests/skills_search.rs`,使用 `mockito` mock:
- `parses_search_results` — 正常解析 + source_url 拼接
- `source_url_is_constructed_from_source` — source → GitHub URL
- `http_error_returns_error` — HTTP 500 错误处理
- `empty_results` — 空结果

## 修改文件清单

| 文件 | 操作 | 说明 |
|------|------|------|
| `src-tauri/src/core/skills_search.rs` | 新建 | 搜索核心逻辑(请求 skills.sh API) |
| `src-tauri/src/core/mod.rs` | 修改 | 导出 `skills_search` 模块 |
| `src-tauri/src/commands/mod.rs` | 修改 | 新增 `OnlineSkillDto` + `search_skills_online` 命令 |
| `src-tauri/src/lib.rs` | 修改 | 在 `generate_handler!` 注册命令 |
| `src-tauri/src/core/tests/skills_search.rs` | 新建 | 4 个后端测试 |
| `src/components/skills/types.ts` | 修改 | 新增 `OnlineSkillDto` 类型 |
| `src/App.tsx` | 修改 | 搜索状态 + 防抖 + `autoSelectSkillName` 自动匹配 |
| `src/components/skills/modals/AddSkillModal.tsx` | 修改 | 分区展示 UI + 去重 |
| `src/i18n/resources.ts` | 修改 | 搜索相关中英文翻译 |
| `src/App.css` | 修改 | 分区标题 + source 样式 |

## 验证方式

1. `npm run check` — 全部通过(lint + build + rust fmt/clippy/test)
2. 打开探索标签,输入 1 个字符 → 仅本地过滤精选列表,无在线搜索区域
3. 输入 2+ 字符 → 精选过滤结果秒出(上方),500ms 后在线搜索区域出现
4. 在线搜索结果中不包含精选列表已有的条目(去重)
5. 搜索结果正确展示(name, installs, source)
6. 清空输入框 → 在线搜索区域消失,恢复完整精选列表
7. 点击搜索结果 → 跳转到 Git 标签填充仓库 URL → 安装时自动匹配目标 skill
8. 多技能仓库自动匹配失败时 → 回退到手动选择弹窗
9. 断网时搜索 → 在线区域显示错误提示,精选列表不受影响

## 已知限制

- skills.sh API 的 `name` 与仓库 SKILL.md frontmatter `name` 不一定一致(如 `json-render-react` vs `react`),自动匹配使用精确 + 唯一包含策略,极端情况可能回退到手动选择
- `source_url` 只包含仓库地址,不含子目录路径(API `id` 字段无法可靠映射到仓库文件路径)
- 多技能仓库首次安装需克隆完整仓库以获取候选列表(后续命中缓存)


================================================
FILE: docs/releases/v0.3.0/plan-skill-detail-view.md
================================================
# 技能详情页(Skill Detail View)— 实施计划(已完成)

## Context

当前已安装的技能以卡片列表展示,但无法查看技能的具体文件内容。用户希望点击技能卡片后能看到技能的所有文件,默认显示 SKILL.md,支持切换文件和返回列表。

### 设计原型

原型图位于 `docs/skills_hub_v2_design.html` 的 Screen 5A–5D 部分。

### 交互流程

1. 在 My Skills 列表中,技能名称为可点击链接(hover 变蓝色)
2. 点击后整个内容区替换为详情视图(非模态框),Header 导航栏保持不变,My Skills tab 保持高亮
3. 详情视图顶部显示返回按钮、技能名称、描述、来源、更新时间、文件数
4. 下方为左右分栏:左侧文件树(260px),右侧文件内容(语法高亮 / Markdown 渲染)
5. 默认选中 SKILL.md(排首位),点击左侧其他文件切换内容
6. 文件夹默认折叠,点击展开/收起
7. 点击返回按钮回到列表视图

## 核心设计决策

### 视图切换而非模态框

采用扩展 `activeView` 状态增加 `'detail'` 视图的方式,与现有的 `'myskills'` / `'explore'` 视图切换机制一致。优势:
- 复用现有视图切换逻辑
- 详情视图可占满整个内容区域,空间充足
- detail 时 Header 仍高亮 My Skills tab,保持导航一致性

### 文件内容渲染策略(三层)

根据文件类型选择不同渲染方式:
1. **Markdown 文件**(`.md`/`.mdx`):使用 `react-markdown` + `remark-gfm` + `remark-frontmatter` 渲染为 GitHub 风格 Markdown(标题、表格、代码块高亮、引用块等),YAML frontmatter 自动剥离不显示
2. **代码文件**(`.ts`/`.js`/`.py`/`.rs` 等 40+ 种语言):使用 `react-syntax-highlighter`(Prism)语法高亮 + 行号,自动检测暗色/亮色主题切换 `oneDark`/`oneLight` 配色
3. **其他文件**:纯文本显示 + 行号

### 左侧文件树(自建,非第三方库)

将扁平路径构建为树形结构,文件夹可折叠/展开,默认折叠。排序:目录在前 → 文件在后,SKILL.md 排首位,其余按字母排序。

### 文件遍历复用 content_hash.rs 的过滤模式

`list_files` 使用与 `content_hash.rs` 相同的 walkdir + IGNORE_NAMES 过滤(排除 `.git`、`.DS_Store` 等),保持一致性。

---

## 步骤一:后端 — 新建 `src-tauri/src/core/skill_files.rs`

复用 `content_hash.rs` 的 walkdir + IGNORE_NAMES 过滤模式。

### 两个函数

**`list_files(central_path: &Path) -> Result<Vec<FileEntry>>`**
- 使用 walkdir 遍历目录,过滤 IGNORE_NAMES
- 返回 `Vec<FileEntry>`(相对路径 + 文件大小)
- SKILL.md 排在首位(排序时特殊处理)
- 其余文件按路径字母排序

**`read_file(central_path: &Path, relative_path: &str) -> Result<String>`**
- 路径穿越防护:禁止包含 `..`,canonicalize 后验证仍在 central_path 内
- 1MB 大小限制,超出返回友好错误信息
- 非 UTF-8 文件返回明确错误提示
- 读取并返回文件内容字符串

### FileEntry 结构体

```rust
pub struct FileEntry {
    pub path: String,  // 相对路径
    pub size: u64,     // 文件大小(字节)
}
```

### 模块导出

在 `src-tauri/src/core/mod.rs` 中添加 `pub mod skill_files;`。

---

## 步骤二:后端 — 新增 Tauri 命令

在 `src-tauri/src/commands/mod.rs` 中:

### DTO

```rust
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SkillFileEntry {
    pub path: String,
    pub size: u64,
}
```

### 命令

```rust
#[tauri::command]
pub async fn list_skill_files(central_path: String) -> Result<Vec<SkillFileEntry>, String>
// 调用 core::skill_files::list_files,转换为 DTO

#[tauri::command]
pub async fn read_skill_file(central_path: String, file_path: String) -> Result<String, String>
// 调用 core::skill_files::read_file
```

### 注册

在 `src-tauri/src/lib.rs` 的 `generate_handler!` 中添加:
- `commands::list_skill_files`
- `commands::read_skill_file`

---

## 步骤三:前端 — 类型定义

在 `src/components/skills/types.ts` 中新增:

```typescript
export type SkillFileEntry = {
  path: string
  size: number
}
```

---

## 步骤四:前端 — 新建 `src/components/skills/SkillDetailView.tsx`

### 依赖

- `react-syntax-highlighter`(Prism 版)— 代码高亮 + 行号,40+ 语言,oneLight/oneDark 主题
- `react-markdown` — Markdown 渲染
- `remark-gfm` — GitHub Flavored Markdown(表格、删除线、任务列表等)
- `remark-frontmatter` — 剥离 YAML frontmatter(SKILL.md 头部 metadata)

### Props

```typescript
type SkillDetailViewProps = {
  skill: ManagedSkill
  onBack: () => void
  invokeTauri: <T>(command: string, args?: Record<string, unknown>) => Promise<T>
  formatRelative: (ms: number | null | undefined) => string
  t: TFunction
}
```

### 内部状态

- `files: SkillFileEntry[]` — 文件列表
- `activeFile: string | null` — 当前选中的文件路径
- `fileContent: string` — 当前文件内容
- `loadingFiles: boolean` — 文件列表加载中
- `loadingContent: boolean` — 文件内容加载中
- `expanded: Set<string>` — 已展开的文件夹路径集合

### 内部子组件

- **`FileTreeNode`**(memo)— 递归渲染文件树节点,文件夹带 ChevronRight/ChevronDown + Folder/FolderOpen 图标
- **`FileContentRenderer`**(memo)— 根据文件类型选择 Markdown / SyntaxHighlighter / 纯文本渲染

### 行为

- `useEffect` 挂载时调用 `invoke('list_skill_files', { centralPath })` 获取文件列表
- 将扁平路径通过 `buildTree()` 构建为树形结构
- 文件夹默认折叠(`expanded` 初始为空 Set)
- 获取到文件列表后,默认选中第一个文件(应为 SKILL.md)
- `activeFile` 变化时调用 `invoke('read_skill_file', { centralPath, filePath })` 读取内容
- 自动检测暗色/亮色主题(读取 `data-theme` 属性),切换语法高亮配色
- 错误处理:通过 toast 显示错误信息

### 布局

```
┌──────────────────────────────────────────────────────┐
│ [← Back]                                              │
│ 技能名称                                               │
│ 描述                                                   │
│ 来源 · 更新时间 · N files                               │
├──────────────┬───────────────────────────────────────┤
│ FILES        │  file-path                      size   │
│ ▸ skills/    │┌─────────────────────────────────────┐│
│ ▾ examples/  ││  Markdown 渲染 / 语法高亮 + 行号     ││
│   basic.md   ││                                     ││
│   adv.md     ││                                     ││
│ SKILL.md ●   │└─────────────────────────────────────┘│
└──────────────┴───────────────────────────────────────┘
```

---

## 步骤五:前端 — 修改 `src/App.tsx`

### 状态变更

- `activeView` 类型扩展为 `'myskills' | 'explore' | 'detail'`
- 新增 `detailSkill: ManagedSkill | null` 状态

### 回调函数

```typescript
const handleOpenDetail = useCallback((skill: ManagedSkill) => {
  setDetailSkill(skill)
  setActiveView('detail')
}, [])

const handleBackToList = useCallback(() => {
  setDetailSkill(null)
  setActiveView('myskills')
}, [])
```

### 渲染

在 `skills-main` 区域增加 `activeView === 'detail'` 分支,渲染 `<SkillDetailView>`。

---

## 步骤六:前端 — 修改 `src/components/skills/SkillCard.tsx`

- 新增 `onOpenDetail: (skill: ManagedSkill) => void` prop
- `.skill-name` 改为 `<button>` 元素,添加 `onClick={() => onOpenDetail(skill)}`
- 添加 `clickable` CSS class(hover 变蓝色)

---

## 步骤七:前端 — 修改 `src/components/skills/SkillsList.tsx`

- 新增 `onOpenDetail` prop
- 透传到每个 `<SkillCard>` 组件

---

## 步骤八:前端 — 修改 `src/components/skills/Header.tsx`

- `activeView` 类型扩展为 `'myskills' | 'explore' | 'detail'`
- detail 视图时 My Skills tab 保持高亮状态(判断条件改为 `activeView === 'myskills' || activeView === 'detail'`)

---

## 步骤九:样式 — `src/App.css`

新增详情视图相关 CSS class:

**布局结构:**
- `.detail-view` — 整体容器(flex column, 填满空间)
- `.detail-header` — 顶部技能信息区
- `.detail-back-btn` — 返回按钮(hover 变蓝)
- `.detail-skill-name` — 技能名称(大号加粗)
- `.detail-desc` — 描述文字
- `.detail-meta` — 来源/时间/文件数元信息行
- `.detail-body` — 左右分栏容器(flex row)

**文件树侧栏(260px):**
- `.detail-file-list` — 侧栏容器(bg-panel 背景)
- `.file-list-title` — "Files" 标题
- `.file-tree` — 树容器
- `.tree-item` — 树节点(通用,28px 最小高度)
- `.tree-dir` — 目录节点
- `.tree-file` — 文件节点(active 蓝色高亮)
- `.tree-chevron` — 折叠箭头
- `.tree-icon-folder` — 文件夹图标(蓝色)
- `.tree-icon-file` — 文件图标
- `.tree-name` — 名称(ellipsis 截断)
- `.tree-size` — 文件大小

**文件内容区:**
- `.detail-file-content` — 右侧内容区
- `.file-content-header` — 文件路径 + 大小(sticky top, bg-panel 背景)
- `.file-content-body` — 内容容器

**Markdown 渲染样式:**
- `.markdown-body` — Markdown 容器(max-width 860px,GitHub 风格排版)
- `.markdown-body h1/h2` — 标题带底部边框
- `.markdown-body pre` — 代码块(border + border-radius)
- `.markdown-body .md-inline-code` — 行内代码
- `.markdown-body table/th/td` — 表格样式
- `.markdown-body blockquote` — 引用块(蓝色左边框)

**通用:**
- `.skill-name.clickable` — 卡片中可点击的技能名称

暗色主题通过 CSS 变量自动适配,无需额外样式。

---

## 步骤十:i18n — `src/i18n/resources.ts`

新增翻译 key(`detail` 命名空间):

| Key | EN | ZH |
|-----|----|----|
| `detail.back` | Back | 返回 |
| `detail.files` | Files | 文件 |
| `detail.noFiles` | No files found | 未找到文件 |
| `detail.loadingFiles` | Loading files... | 加载文件中... |
| `detail.loadingContent` | Loading file content... | 加载文件内容中... |
| `detail.readError` | Failed to read file | 读取文件失败 |
| `detail.fileCount` | {{count}} files | {{count}} 个文件 |

---

## 新增依赖

| 包名 | 用途 |
|------|------|
| `react-syntax-highlighter` | 代码语法高亮 + 行号(Prism 版,oneLight/oneDark 主题) |
| `@types/react-syntax-highlighter` | TypeScript 类型定义 |
| `react-markdown` | Markdown 渲染 |
| `remark-gfm` | GitHub Flavored Markdown 支持 |
| `remark-frontmatter` | YAML frontmatter 剥离 |

---

## 修改文件清单

| 文件 | 操作 | 说明 |
|------|------|------|
| `package.json` | 修改 | 新增 5 个依赖 |
| `src-tauri/src/core/skill_files.rs` | **新建** | list_files + read_file 核心逻辑 |
| `src-tauri/src/core/mod.rs` | 修改 | 导出 skill_files 模块 |
| `src-tauri/src/commands/mod.rs` | 修改 | 新增 2 个命令 + SkillFileEntry DTO |
| `src-tauri/src/lib.rs` | 修改 | 注册 list_skill_files、read_skill_file |
| `src/components/skills/types.ts` | 修改 | 新增 SkillFileEntry 类型 |
| `src/components/skills/SkillDetailView.tsx` | **新建** | 详情视图组件(文件树 + Markdown/高亮渲染) |
| `src/components/skills/SkillCard.tsx` | 修改 | 新增 onOpenDetail prop + 点击事件 |
| `src/components/skills/SkillsList.tsx` | 修改 | 透传 onOpenDetail prop |
| `src/components/skills/Header.tsx` | 修改 | activeView 类型扩展 |
| `src/App.tsx` | 修改 | 新增状态 + 渲染分支 + import SkillDetailView |
| `src/App.css` | 修改 | 新增文件树 + Markdown + 详情视图样式 |
| `src/i18n/resources.ts` | 修改 | 新增翻译 key(中英双语) |

---

## 验证

1. `npm run check` — 确保 lint + build + rust clippy/test 全部通过 ✅
2. `npm run tauri:dev` — 手动测试:
   - 点击技能名称进入详情视图
   - 默认显示 SKILL.md 内容(Markdown 渲染,frontmatter 已剥离)
   - 切换代码文件,语法高亮 + 行号正确显示
   - 文件树目录可折叠/展开,默认折叠
   - 点击返回按钮回到列表
   - Header 导航状态正确(detail 时 My Skills 高亮)
   - 暗色/亮色主题下显示正常
   - 加载状态正确显示


================================================
FILE: docs/releases/v0.3.1/plan-in-app-update.md
================================================
# 需求:应用内检查更新(Issue #33)

## Context

来源:https://github.com/qufei1993/skills-hub/issues/33

用户每次跟进版本都需要手动进入 GitHub releases 页面下载,体验不便。需要在软件内支持手动检查更新功能。

## 现状分析

后端 Tauri updater 插件已完全就绪:
- `tauri-plugin-updater` v2 已安装(Cargo.toml + package.json)
- `tauri.conf.json` 已配置更新端点(GitHub releases)+ 公钥签名验证
- `lib.rs` 已注册插件
- i18n 翻译键已全部就绪(EN/ZH)

唯一缺失:前端没有调用更新 API 的 UI。

## 实施方案

在 SettingsModal 底部版本信息区域扩展为"检查更新"功能块:

1. **SettingsModal.tsx** — 添加更新状态管理 + UI
   - 状态:idle → checking → up-to-date / available → downloading → done / error
   - 使用 `@tauri-apps/plugin-updater` 的 `check()` 和 `downloadAndInstall()` API
   - 保存 update 对象引用避免重复请求
   - 弹窗关闭时重置状态

2. **App.css** — 添加更新区块样式
   - 版本号 + 按钮水平排列
   - 更新可用时显示高亮区块
   - 错误/成功状态样式

3. **版本号** — 0.3.0 → 0.3.1

## 涉及文件

| 文件 | 改动 |
|------|------|
| `src/components/skills/modals/SettingsModal.tsx` | 添加更新检查 UI + 逻辑 |
| `src/App.css` | 添加更新区块样式 |
| `package.json` | 版本号 → 0.3.1 |
| `src-tauri/tauri.conf.json` | 版本号 → 0.3.1 |
| `src-tauri/Cargo.toml` | 版本号 → 0.3.1 |


================================================
FILE: docs/releases/v0.3.1/plan-qoderwork-support.md
================================================
# 需求:支持 QoderWork 目录(Issue #34)

## Context

来源:https://github.com/qufei1993/skills-hub/issues/34

QoderWork 是 Qoder 推出的桌面 AI 代理产品,与 Qoder IDE 独立,使用 `~/.qoderwork/skills/` 目录存放技能。当前代码库已支持 Qoder(`.qoder/skills`),但尚未支持 QoderWork。

## 实施方案

参照现有 Qoder 适配器模式,在 3 处添加 QoderWork 支持:

### 1. `src-tauri/src/core/tool_adapters/mod.rs`

- **ToolId 枚举**:在 `Qoder` 之后添加 `QoderWork`
- **as_key() 方法**:添加 `ToolId::QoderWork => "qoderwork"`
- **default_tool_adapters()**:添加 ToolAdapter 实例:
  ```rust
  ToolAdapter {
      id: ToolId::QoderWork,
      display_name: "QoderWork",
      relative_skills_dir: ".qoderwork/skills",
      relative_detect_dir: ".qoderwork",
  },
  ```

### 2. `src/i18n/resources.ts`

- 英文 tools 对象:添加 `qoderwork: 'QoderWork'`
- 中文 tools 对象:添加 `qoderwork: 'QoderWork'`

## 验证

- `npm run check` 确保 lint、build、Rust clippy/test 全部通过


================================================
FILE: docs/releases/v0.3.1/plan-settings-page.md
================================================
# 需求:设置弹窗改为独立页面

## Context

设置功能原来是一个 560px 宽的模态弹窗(SettingsModal),在小窗口下容易被遮挡,需要最大化窗口才能完整查看。将其改为 `activeView` 视图系统中的独立页面,与 myskills/explore/detail 并列,UX 更自然。

## 实施方案

### 1. 组件迁移:`modals/SettingsModal.tsx` → `SettingsPage.tsx`

- 移动到 `src/components/skills/SettingsPage.tsx`,与 `ExplorePage`、`SkillDetailView` 同级
- 类型名 `SettingsModalProps` → `SettingsPageProps`
  - 删除 `open: boolean`
  - `onRequestClose` → `onBack`
- 去掉 modal 外壳(`modal-backdrop` / `modal` / `modal-header` / `modal-footer`)
- 复用 `detail-header` + `detail-back-btn` 样式,与 SkillDetailView 保持一致
- useEffect 从依赖 `open` 改为挂载/卸载模式
- 删除 `if (!open) return null` 守卫
- 删除底部 "Done" 按钮

### 2. `App.tsx` — 状态和路由

- `activeView` 类型扩展为 `'myskills' | 'explore' | 'detail' | 'settings'`
- 删除 `showSettingsModal` 布尔状态
- `handleOpenSettings` 改为 `setActiveView('settings')`
- `handleCloseSettings` 改为 `setActiveView('myskills')`
- `<main>` 条件渲染新增 `settings` 分支,渲染 `<SettingsPage>`
- 删除底部的 `<SettingsModal>` 渲染块

### 3. `Header.tsx` — 导航状态

- `activeView` 类型加 `| 'settings'`
- 设置齿轮按钮在 `activeView === 'settings'` 时添加 `active` 样式

### 4. `App.css` — 样式调整

- 删除 `.settings-modal` 和 `.settings-body` 规则
- 新增 `.settings-page`(居中布局)和 `.settings-page-body`(560px 最大宽度,居中)
- 复用 `.detail-header` / `.detail-back-btn` / `.detail-skill-name` 样式
- 所有 `.settings-field` 等子样式保持不变

### 不需要改动

- **i18n**:复用已有的 `detail.back`、`settings` key
- **Rust 后端**:纯前端改动
- **types.ts**:无 DTO 变更


================================================
FILE: docs/releases/v0.3.1/skills-aggregation-repo.md
================================================
# 需求:Skills 聚合数据源升级 — 精选仓库列表方案

## 背景

Skills Hub 应用的"探索"功能依赖 `featured-skills.json` 提供精选技能列表。早期方案通过 GitHub Search API 搜索 `claude-code-skill` 等 topic 标签自动发现仓库,但属于盲目搜索,质量不可控,噪音多。

**新方案**:维护一份精选仓库列表(Curated Repos),直接从已知的高质量仓库中获取 skills。质量可控、API 调用极少、维护简单。

## 目标

重写 `scripts/fetch-featured-skills.mjs` 脚本,从精选仓库列表直接获取元数据并深入检测 skill 目录结构,为每个 skill 生成独立条目,最终输出 `featured-skills.json`(最多 300 条)。保持现有的 GitHub Actions 定时更新机制不变。

## 架构(保持不变)

```
skills-desktop-app/
├── featured-skills.json                          # 精选技能数据(应用内嵌 fallback + 在线更新源)
├── scripts/
│   └── fetch-featured-skills.mjs                 # 聚合脚本(需重写)
└── .github/workflows/
    └── update-featured-skills.yml                # 每日定时运行(保持不变)
```

## 数据源 — 精选仓库列表

从 GitHub Topic 盲目搜索改为直接指定高质量仓库:

| 仓库 | Stars | 说明 |
|------|-------|------|
| `anthropics/skills` | ~96k | Anthropic 官方 Agent Skills |
| `sickn33/antigravity-awesome-skills` | ~24.7k | 1000+ 社区 skills 集合 |
| `K-Dense-AI/claude-scientific-skills` | ~15.3k | 170+ 科学研究 skills |
| `travisvn/awesome-claude-skills` | ~9.1k | 精选 Claude skills 列表 |
| `VoltAgent/awesome-agent-skills` | ~8.4k | 500+ Agent skills |
| `anthropics/knowledge-work-plugins` | ~7.7k | 官方知识工作插件 |
| `alirezarezvani/claude-skills` | ~5.2k | 192+ 社区 skills |

列表定义在脚本顶部 `CURATED_REPOS` 常量中,新增/移除仓库只需编辑此数组。

## 脚本重写逻辑

### 1. 数据采集(仓库元数据)

通过 GitHub Repos API 逐个获取精选仓库的元数据:

```
GET /repos/{owner}/{repo}
```

返回:`full_name`, `description`, `stargazers_count`, `topics[]`, `updated_at`, `html_url`, `default_branch`

认证:**必须**使用环境变量 `GITHUB_TOKEN`,脚本启动时校验。

失败处理:获取失败的仓库跳过(打印警告),不影响其余仓库。

### 2. Skill 检测(仓库内深入扫描)

使用 GitHub Git Trees API 获取仓库目录结构(单次请求,`recursive=1`):

```
GET /repos/{owner}/{repo}/git/trees/{default_branch}?recursive=1
```

#### Skill 目录扫描规则(与应用端 `installer.rs` 保持一致)

扫描以下基础路径下的子目录:

```
skills/
skills/.curated/
skills/.experimental/
skills/.system/
.claude/skills/
```

以及**根目录的直接子目录**(排除 `skills/`、`.claude/`、`.git/` 等特殊目录)。

#### 新增:`.claude-plugin/plugin.json` 检测

为兼容 `anthropics/knowledge-work-plugins` 等使用插件格式的仓库,增加检测规则:

- 根级子目录包含 `.claude-plugin/plugin.json` 文件 → 视为有效 skill

#### Skill 判定条件

一个目录被视为有效 skill,需满足以下任一条件:
- 目录内存在 `SKILL.md` 文件
- 目录位于 `.claude/skills/` 路径下(即使没有 `SKILL.md`)
- 目录内存在 `.claude-plugin/plugin.json` 文件(插件格式)

#### 单 skill 仓库

如果仓库根目录本身就是一个 skill(根目录有 `SKILL.md`),且没有检测到子目录 skill,则整个仓库视为单 skill,`source_url` 指向仓库根路径。

#### Skill 名称与描述

- **名称**:从 skill 目录名生成(kebab-case → Title Case)
- **描述**:使用仓库的 `description` 字段(同仓库内所有 skill 共享)

> 注:不使用 Contents API 获取 SKILL.md 内容,避免大量 API 调用。目录名 + 仓库 description 已满足展示需求。

### 3. 自动分类

基于仓库 `topics[]` 和 `description` 关键词匹配(分类继承自仓库,同仓库内所有 skill 共享分类):

| 关键词 | 分类 |
|--------|------|
| browser, automation, playwright, puppeteer | browser-automation |
| security, audit, vulnerability, pentest | security |
| devops, deploy, infra, docker, kubernetes | devops |
| marketing, seo, ads, advertising | marketing |
| database, sql, postgres, mongo | database |
| git, github, pr, code-review | development |
| ai, llm, agent, model | ai-assistant |
| 以上都不匹配 | general |

### 4. 排序与截取

1. 按仓库 `stargazers_count` 降序,同星数按 skill 名称字母序
2. **截取前 300 条**(`MAX_SKILLS = 300`),避免输出过大

### 5. 输出

生成 `featured-skills.json`,**向前兼容现有数据结构**:

```json
{
  "updated_at": "2026-03-19T00:00:00Z",
  "total": 300,
  "categories": ["general", "browser-automation", "security", "devops", ...],
  "skills": [
    {
      "slug": "commit",
      "name": "Conventional Commit",
      "summary": "Generate conventional commit messages...",
      "downloads": 0,
      "stars": 96000,
      "category": "development",
      "tags": ["claude-code-skill", "git", "commit"],
      "source_url": "https://github.com/anthropics/skills/tree/main/skills/commit",
      "updated_at": "2026-03-17T15:10:09Z"
    }
  ]
}
```

#### 向前兼容策略

新格式**必须保留现有字段**,确保后端 `FeaturedSkillRaw` 和前端 `FeaturedSkillDto` 无需任何改动即可解析新数据:

| 字段 | 现有 | 新版 | 兼容处理 |
|------|------|------|----------|
| `slug` | ✅ | ✅ | 不变 |
| `name` | ✅ | ✅ | 不变 |
| `summary` | ✅ | ✅ | 不变 |
| `downloads` | ✅ | ✅ | **保留,固定为 0**(无真实数据源) |
| `stars` | ✅ | ✅ | 填入 GitHub 真实 star 数 |
| `source_url` | ✅ | ✅ | 精确到 skill 目录的路径 |
| `category` | ❌ | ✅ 新增 | 后端 `#[serde(default)]` 自动忽略未知字段 |
| `tags` | ❌ | ✅ 新增 | 同上 |
| `updated_at`(skill 级) | ❌ | ✅ 新增 | 同上 |

**结论**:脚本输出格式升级后,后端和前端代码**零改动**即可正常工作。

#### 字段说明

- `slug`:skill 目录名(单 skill 仓库则为仓库名)
- `name`:基于 skill 目录名生成(kebab-case → Title Case)
- `summary`:仓库 `description`(同仓库内所有 skill 共享)
- `downloads`:固定为 `0`(向前兼容,无真实数据源)
- `stars`:所属仓库的 GitHub star 数
- `category`:基于仓库 topics/description 的自动分类结果
- `tags`:仓库 `topics[]`(最多取 5 个)
- `source_url`:**精确到 skill 目录的 GitHub URL**,格式为 `https://github.com/{owner}/{repo}/tree/{branch}/{skill-path}`;单 skill 仓库则为 `https://github.com/{owner}/{repo}`
- `updated_at`:仓库最后更新时间

## API 用量估算

| 阶段 | API | 请求数 | 说明 |
|------|-----|--------|------|
| 获取仓库元数据 | Repos API | 7 | 每个精选仓库 1 次 |
| 获取目录树 | Git Trees API | 7 | 每个仓库 1 次 |
| **合计** | | **14** | |

对比旧方案(Topic 搜索)的 ~412 次请求,新方案仅需 14 次,**几乎不可能触发速率限制**。

GitHub API 限额(已认证):5000 次/小时。即使未认证(60 次/小时)也完全够用,但仍建议使用 token 以确保稳定性。

## GitHub Actions 配置

保持现有 `.github/workflows/update-featured-skills.yml` 不变:

```yaml
- name: Fetch featured skills
  run: node scripts/fetch-featured-skills.mjs
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

## Skills Hub 应用端适配

**后端和前端代码无需任何改动**(向前兼容):

- `FeaturedSkillRaw` / `FeaturedSkillDto` 结构体不变
- 新增的 `category`、`tags`、`updated_at` 等字段被 serde 自动忽略
- 内嵌 fallback(`featured-skills.json`)随脚本运行自动更新

### 后续可选增强(独立需求)

1. 后端 DTO 新增 `category`、`tags` 字段以支持前端展示
2. 探索页面增加按分类筛选
3. 排序改为按星数排序

## 技术要点

- **精选仓库列表**:质量可控,新增仓库只需编辑 `CURATED_REPOS` 数组
- **极低 API 用量**:14 次请求 vs 旧方案 412 次,无速率限制风险
- **零外部依赖**:仅依赖 GitHub API(公开、稳定、有 SLA)
- **Skill 粒度聚合**:每条记录精确到仓库内具体 skill 目录
- **与应用检测逻辑一致**:skill 扫描规则与 `installer.rs` 保持同步
- **新增插件格式支持**:兼容 `.claude-plugin/plugin.json` 结构
- **数量上限**:最多 300 条,按星数排序取 top
- **项目内维护**:脚本、数据、CI 全部在 skills-desktop-app 仓库内

## 变更清单

| 文件 | 操作 |
|------|------|
| `scripts/fetch-featured-skills.mjs` | 重写(精选仓库列表 + Repos API + Trees API) |
| `featured-skills.json` | 内容更新(精选仓库来源,≤300 条) |
| `docs/requirements/skills-aggregation-repo.md` | 更新需求文档 |
| `.github/workflows/update-featured-skills.yml` | **无需改动** |
| 后端代码 | **无需改动**(向前兼容) |
| 前端代码 | **无需改动**(向前兼容) |


================================================
FILE: docs/releases/v0.4.0/bugfix-language-toggle-loading-overlay.md
================================================
# Bugfix:切换语言时闪现 "Installing Skills..." 弹窗

## 问题描述

在 Explore 页面点击语言切换按钮(EN/中)时,会短暂弹出 "Installing Skills..." 加载遮罩,一两秒后自动消失。

## 根因分析

`invokeTauri` 函数的 `useCallback` 依赖数组包含了 `t`(i18next 翻译函数):

```ts
const invokeTauri = useCallback(async (...) => {
  if (!isTauri) throw new Error(t('errors.notTauri'))
  ...
}, [isTauri, t])  // ← t 导致级联重建
```

切换语言时的级联链:

1. `i18n.changeLanguage()` → `t` 引用变化
2. `t` 变化 → `invokeTauri` 重新创建(依赖 `t`)
3. `invokeTauri` 变化 → `loadPlan` 重新创建(依赖 `invokeTauri`)
4. `loadPlan` 变化 → `useEffect([isTauri, loadPlan])` 重新触发
5. `loadPlan()` 执行 → `setLoading(true)` → 显示加载弹窗
6. 请求完成 → `setLoading(false)` → 弹窗消失

## 修复方案

将 `invokeTauri` 中的 `t('errors.notTauri')` 替换为硬编码字符串,从依赖数组移除 `t`:

```ts
const invokeTauri = useCallback(async (...) => {
  if (!isTauri) throw new Error('Tauri API is not available')
  ...
}, [isTauri])  // ← 不再依赖 t
```

该错误消息仅在非 Tauri 环境下触发(实际不可能发生),无需翻译。

## 修改文件

- `src/App.tsx`:`invokeTauri` useCallback 依赖数组移除 `t`


================================================
FILE: docs/releases/v0.4.0/plan-in-app-update.md
================================================
# 需求:应用内检查更新(Issue #33)

## Context

来源:https://github.com/qufei1993/skills-hub/issues/33

用户每次跟进版本都需要手动进入 GitHub releases 页面下载,体验不便。需要在软件内支持手动检查更新功能。

## 现状分析

后端 Tauri updater 插件已完全就绪:
- `tauri-plugin-updater` v2 已安装(Cargo.toml + package.json)
- `tauri.conf.json` 已配置更新端点(GitHub releases)+ 公钥签名验证
- `lib.rs` 已注册插件
- i18n 翻译键已全部就绪(EN/ZH)

唯一缺失:前端没有调用更新 API 的 UI。

## 实施方案

在 SettingsModal 底部版本信息区域扩展为"检查更新"功能块:

1. **SettingsModal.tsx** — 添加更新状态管理 + UI
   - 状态:idle → checking → up-to-date / available → downloading → done / error
   - 使用 `@tauri-apps/plugin-updater` 的 `check()` 和 `downloadAndInstall()` API
   - 保存 update 对象引用避免重复请求
   - 弹窗关闭时重置状态

2. **App.css** — 添加更新区块样式
   - 版本号 + 按钮水平排列
   - 更新可用时显示高亮区块
   - 错误/成功状态样式

3. **版本号** — 0.3.0 → 0.3.1

## 涉及文件

| 文件 | 改动 |
|------|------|
| `src/components/skills/modals/SettingsModal.tsx` | 添加更新检查 UI + 逻辑 |
| `src/App.css` | 添加更新区块样式 |
| `package.json` | 版本号 → 0.3.1 |
| `src-tauri/tauri.conf.json` | 版本号 → 0.3.1 |
| `src-tauri/Cargo.toml` | 版本号 → 0.3.1 |


================================================
FILE: docs/releases/v0.4.0/plan-qoderwork-support.md
================================================
# 需求:支持 QoderWork 目录(Issue #34)

## Context

来源:https://github.com/qufei1993/skills-hub/issues/34

QoderWork 是 Qoder 推出的桌面 AI 代理产品,与 Qoder IDE 独立,使用 `~/.qoderwork/skills/` 目录存放技能。当前代码库已支持 Qoder(`.qoder/skills`),但尚未支持 QoderWork。

## 实施方案

参照现有 Qoder 适配器模式,在 3 处添加 QoderWork 支持:

### 1. `src-tauri/src/core/tool_adapters/mod.rs`

- **ToolId 枚举**:在 `Qoder` 之后添加 `QoderWork`
- **as_key() 方法**:添加 `ToolId::QoderWork => "qoderwork"`
- **default_tool_adapters()**:添加 ToolAdapter 实例:
  ```rust
  ToolAdapter {
      id: ToolId::QoderWork,
      display_name: "QoderWork",
      relative_skills_dir: ".qoderwork/skills",
      relative_detect_dir: ".qoderwork",
  },
  ```

### 2. `src/i18n/resources.ts`

- 英文 tools 对象:添加 `qoderwork: 'QoderWork'`
- 中文 tools 对象:添加 `qoderwork: 'QoderWork'`

## 验证

- `npm run check` 确保 lint、build、Rust clippy/test 全部通过


================================================
FILE: docs/releases/v0.4.0/plan-settings-page.md
================================================
# 需求:设置弹窗改为独立页面

## Context

设置功能原来是一个 560px 宽的模态弹窗(SettingsModal),在小窗口下容易被遮挡,需要最大化窗口才能完整查看。将其改为 `activeView` 视图系统中的独立页面,与 myskills/explore/detail 并列,UX 更自然。

## 实施方案

### 1. 组件迁移:`modals/SettingsModal.tsx` → `SettingsPage.tsx`

- 移动到 `src/components/skills/SettingsPage.tsx`,与 `ExplorePage`、`SkillDetailView` 同级
- 类型名 `SettingsModalProps` → `SettingsPageProps`
  - 删除 `open: boolean`
  - `onRequestClose` → `onBack`
- 去掉 modal 外壳(`modal-backdrop` / `modal` / `modal-header` / `modal-footer`)
- 复用 `detail-header` + `detail-back-btn` 样式,与 SkillDetailView 保持一致
- useEffect 从依赖 `open` 改为挂载/卸载模式
- 删除 `if (!open) return null` 守卫
- 删除底部 "Done" 按钮

### 2. `App.tsx` — 状态和路由

- `activeView` 类型扩展为 `'myskills' | 'explore' | 'detail' | 'settings'`
- 删除 `showSettingsModal` 布尔状态
- `handleOpenSettings` 改为 `setActiveView('settings')`
- `handleCloseSettings` 改为 `setActiveView('myskills')`
- `<main>` 条件渲染新增 `settings` 分支,渲染 `<SettingsPage>`
- 删除底部的 `<SettingsModal>` 渲染块

### 3. `Header.tsx` — 导航状态

- `activeView` 类型加 `| 'settings'`
- 设置齿轮按钮在 `activeView === 'settings'` 时添加 `active` 样式

### 4. `App.css` — 样式调整

- 删除 `.settings-modal` 和 `.settings-body` 规则
- 新增 `.settings-page`(居中布局)和 `.settings-page-body`(560px 最大宽度,居中)
- 复用 `.detail-header` / `.detail-back-btn` / `.detail-skill-name` 样式
- 所有 `.settings-field` 等子样式保持不变

### 不需要改动

- **i18n**:复用已有的 `detail.back`、`settings` key
- **Rust 后端**:纯前端改动
- **types.ts**:无 DTO 变更


================================================
FILE: docs/releases/v0.4.0/skills-aggregation-repo.md
================================================
# 需求:Skills 聚合数据源升级 — 精选仓库列表方案

## 背景

Skills Hub 应用的"探索"功能依赖 `featured-skills.json` 提供精选技能列表。早期方案通过 GitHub Search API 搜索 `claude-code-skill` 等 topic 标签自动发现仓库,但属于盲目搜索,质量不可控,噪音多。

**新方案**:维护一份精选仓库列表(Curated Repos),直接从已知的高质量仓库中获取 skills。质量可控、API 调用极少、维护简单。

## 目标

重写 `scripts/fetch-featured-skills.mjs` 脚本,从精选仓库列表直接获取元数据并深入检测 skill 目录结构,为每个 skill 生成独立条目,最终输出 `featured-skills.json`(最多 300 条)。保持现有的 GitHub Actions 定时更新机制不变。

## 架构(保持不变)

```
skills-desktop-app/
├── featured-skills.json                          # 精选技能数据(应用内嵌 fallback + 在线更新源)
├── scripts/
│   └── fetch-featured-skills.mjs                 # 聚合脚本(需重写)
└── .github/workflows/
    └── update-featured-skills.yml                # 每日定时运行(保持不变)
```

## 数据源 — 精选仓库列表

从 GitHub Topic 盲目搜索改为直接指定高质量仓库:

| 仓库 | Stars | 说明 |
|------|-------|------|
| `anthropics/skills` | ~96k | Anthropic 官方 Agent Skills |
| `sickn33/antigravity-awesome-skills` | ~24.7k | 1000+ 社区 skills 集合 |
| `K-Dense-AI/claude-scientific-skills` | ~15.3k | 170+ 科学研究 skills |
| `travisvn/awesome-claude-skills` | ~9.1k | 精选 Claude skills 列表 |
| `VoltAgent/awesome-agent-skills` | ~8.4k | 500+ Agent skills |
| `anthropics/knowledge-work-plugins` | ~7.7k | 官方知识工作插件 |
| `alirezarezvani/claude-skills` | ~5.2k | 192+ 社区 skills |

列表定义在脚本顶部 `CURATED_REPOS` 常量中,新增/移除仓库只需编辑此数组。

## 脚本重写逻辑

### 1. 数据采集(仓库元数据)

通过 GitHub Repos API 逐个获取精选仓库的元数据:

```
GET /repos/{owner}/{repo}
```

返回:`full_name`, `description`, `stargazers_count`, `topics[]`, `updated_at`, `html_url`, `default_branch`

认证:**必须**使用环境变量 `GITHUB_TOKEN`,脚本启动时校验。

失败处理:获取失败的仓库跳过(打印警告),不影响其余仓库。

### 2. Skill 检测(仓库内深入扫描)

使用 GitHub Git Trees API 获取仓库目录结构(单次请求,`recursive=1`):

```
GET /repos/{owner}/{repo}/git/trees/{default_branch}?recursive=1
```

#### Skill 目录扫描规则(与应用端 `installer.rs` 保持一致)

扫描以下基础路径下的子目录:

```
skills/
skills/.curated/
skills/.experimental/
skills/.system/
.claude/skills/
```

以及**根目录的直接子目录**(排除 `skills/`、`.claude/`、`.git/` 等特殊目录)。

#### 新增:`.claude-plugin/plugin.json` 检测

为兼容 `anthropics/knowledge-work-plugins` 等使用插件格式的仓库,增加检测规则:

- 根级子目录包含 `.claude-plugin/plugin.json` 文件 → 视为有效 skill

#### Skill 判定条件

一个目录被视为有效 skill,需满足以下任一条件:
- 目录内存在 `SKILL.md` 文件
- 目录位于 `.claude/skills/` 路径下(即使没有 `SKILL.md`)
- 目录内存在 `.claude-plugin/plugin.json` 文件(插件格式)

#### 单 skill 仓库

如果仓库根目录本身就是一个 skill(根目录有 `SKILL.md`),且没有检测到子目录 skill,则整个仓库视为单 skill,`source_url` 指向仓库根路径。

#### Skill 名称与描述

- **名称**:从 skill 目录名生成(kebab-case → Title Case)
- **描述**:使用仓库的 `description` 字段(同仓库内所有 skill 共享)

> 注:不使用 Contents API 获取 SKILL.md 内容,避免大量 API 调用。目录名 + 仓库 description 已满足展示需求。

### 3. 自动分类

基于仓库 `topics[]` 和 `description` 关键词匹配(分类继承自仓库,同仓库内所有 skill 共享分类):

| 关键词 | 分类 |
|--------|------|
| browser, automation, playwright, puppeteer | browser-automation |
| security, audit, vulnerability, pentest | security |
| devops, deploy, infra, docker, kubernetes | devops |
| marketing, seo, ads, advertising | marketing |
| database, sql, postgres, mongo | database |
| git, github, pr, code-review | development |
| ai, llm, agent, model | ai-assistant |
| 以上都不匹配 | general |

### 4. 排序与截取

1. 按仓库 `stargazers_count` 降序,同星数按 skill 名称字母序
2. **截取前 300 条**(`MAX_SKILLS = 300`),避免输出过大

### 5. 输出

生成 `featured-skills.json`,**向前兼容现有数据结构**:

```json
{
  "updated_at": "2026-03-19T00:00:00Z",
  "total": 300,
  "categories": ["general", "browser-automation", "security", "devops", ...],
  "skills": [
    {
      "slug": "commit",
      "name": "Conventional Commit",
      "summary": "Generate conventional commit messages...",
      "downloads": 0,
      "stars": 96000,
      "category": "development",
      "tags": ["claude-code-skill", "git", "commit"],
      "source_url": "https://github.com/anthropics/skills/tree/main/skills/commit",
      "updated_at": "2026-03-17T15:10:09Z"
    }
  ]
}
```

#### 向前兼容策略

新格式**必须保留现有字段**,确保后端 `FeaturedSkillRaw` 和前端 `FeaturedSkillDto` 无需任何改动即可解析新数据:

| 字段 | 现有 | 新版 | 兼容处理 |
|------|------|------|----------|
| `slug` | ✅ | ✅ | 不变 |
| `name` | ✅ | ✅ | 不变 |
| `summary` | ✅ | ✅ | 不变 |
| `downloads` | ✅ | ✅ | **保留,固定为 0**(无真实数据源) |
| `stars` | ✅ | ✅ | 填入 GitHub 真实 star 数 |
| `source_url` | ✅ | ✅ | 精确到 skill 目录的路径 |
| `category` | ❌ | ✅ 新增 | 后端 `#[serde(default)]` 自动忽略未知字段 |
| `tags` | ❌ | ✅ 新增 | 同上 |
| `updated_at`(skill 级) | ❌ | ✅ 新增 | 同上 |

**结论**:脚本输出格式升级后,后端和前端代码**零改动**即可正常工作。

#### 字段说明

- `slug`:skill 目录名(单 skill 仓库则为仓库名)
- `name`:基于 skill 目录名生成(kebab-case → Title Case)
- `summary`:仓库 `description`(同仓库内所有 skill 共享)
- `downloads`:固定为 `0`(向前兼容,无真实数据源)
- `stars`:所属仓库的 GitHub star 数
- `category`:基于仓库 topics/description 的自动分类结果
- `tags`:仓库 `topics[]`(最多取 5 个)
- `source_url`:**精确到 skill 目录的 GitHub URL**,格式为 `https://github.com/{owner}/{repo}/tree/{branch}/{skill-path}`;单 skill 仓库则为 `https://github.com/{owner}/{repo}`
- `updated_at`:仓库最后更新时间

## API 用量估算

| 阶段 | API | 请求数 | 说明 |
|------|-----|--------|------|
| 获取仓库元数据 | Repos API | 7 | 每个精选仓库 1 次 |
| 获取目录树 | Git Trees API | 7 | 每个仓库 1 次 |
| **合计** | | **14** | |

对比旧方案(Topic 搜索)的 ~412 次请求,新方案仅需 14 次,**几乎不可能触发速率限制**。

GitHub API 限额(已认证):5000 次/小时。即使未认证(60 次/小时)也完全够用,但仍建议使用 token 以确保稳定性。

## GitHub Actions 配置

保持现有 `.github/workflows/update-featured-skills.yml` 不变:

```yaml
- name: Fetch featured skills
  run: node scripts/fetch-featured-skills.mjs
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

## Skills Hub 应用端适配

**后端和前端代码无需任何改动**(向前兼容):

- `FeaturedSkillRaw` / `FeaturedSkillDto` 结构体不变
- 新增的 `category`、`tags`、`updated_at` 等字段被 serde 自动忽略
- 内嵌 fallback(`featured-skills.json`)随脚本运行自动更新

### 后续可选增强(独立需求)

1. 后端 DTO 新增 `category`、`tags` 字段以支持前端展示
2. 探索页面增加按分类筛选
3. 排序改为按星数排序

## 技术要点

- **精选仓库列表**:质量可控,新增仓库只需编辑 `CURATED_REPOS` 数组
- **极低 API 用量**:14 次请求 vs 旧方案 412 次,无速率限制风险
- **零外部依赖**:仅依赖 GitHub API(公开、稳定、有 SLA)
- **Skill 粒度聚合**:每条记录精确到仓库内具体 skill 目录
- **与应用检测逻辑一致**:skill 扫描规则与 `installer.rs` 保持同步
- **新增插件格式支持**:兼容 `.claude-plugin/plugin.json` 结构
- **数量上限**:最多 300 条,按星数排序取 top
- **项目内维护**:脚本、数据、CI 全部在 skills-desktop-app 仓库内

## 变更清单

| 文件 | 操作 |
|------|------|
| `scripts/fetch-featured-skills.mjs` | 重写(精选仓库列表 + Repos API + Trees API) |
| `featured-skills.json` | 内容更新(精选仓库来源,≤300 条) |
| `docs/requirements/skills-aggregation-repo.md` | 更新需求文档 |
| `.github/workflows/update-featured-skills.yml` | **无需改动** |
| 后端代码 | **无需改动**(向前兼容) |
| 前端代码 | **无需改动**(向前兼容) |


================================================
FILE: docs/releases/v0.4.1/plan-frontmatter-table.md
================================================
# Plan: Skill 详情页 Frontmatter 元数据表格展示

## Context
当前 `SkillDetailView` 使用 `remarkFrontmatter` 插件,该插件只是让 react-markdown 忽略 YAML frontmatter(不报错),但不会渲染它。用户希望像 GitHub 一样,将 frontmatter 中的字段以表格形式展示在 markdown 内容顶部。

## 修改方案

### 仅需修改 1 个文件
**`src/components/skills/SkillDetailView.tsx`** — `FileContentRenderer` 组件

### 实现步骤

1. **添加 frontmatter 解析函数** — 在 `FileContentRenderer` 中,对 markdown 文件的 content 进行简单的 YAML frontmatter 提取:
   - 检测 `---` 开头和结束标记
   - 逐行解析 `key: value` 对,得到 `Record<string, string>`
   - 分离出 frontmatter 数据和剩余的 markdown body

2. **渲染 frontmatter 表格** — 在 `<Markdown>` 组件之前,如果存在 frontmatter 数据,渲染一个 HTML 表格:
   - 表头为 frontmatter 的 key(如 name, description, license)
   - 表体为对应的 value
   - 复用现有的 `.markdown-body table/th/td` 样式(已在 App.css 中定义)

3. **传递剩余内容给 Markdown** — 将去除 frontmatter 后的 body 传给 `<Markdown>` 组件,同时保留 `remarkFrontmatter` 插件(作为安全兜底)

### 不需要的改动
- 不需要安装新依赖(不用 `gray-matter` 等库,简单的字符串解析即可)
- 不需要修改后端
- 不需要新增 CSS(已有 table 样式)
- 不需要 i18n(表头直接使用 frontmatter 的 key 名)

## 验证
- `npm run check` 通过
- 打开一个包含 frontmatter 的 skill(如 SKILL.md),确认顶部显示元数据表格,下方正常渲染 markdown 内容
- 打开没有 frontmatter 的文件,确认不会显示表格,渲染正常


================================================
FILE: docs/releases/v0.4.2/bugfix-new-tools-modal-style.md
================================================
# Bugfix:首次安装后打开时"检测到新工具"弹窗样式异常

## 问题描述

安装后第一次打开 Skills Hub,"New tools detected"(检测到新工具)弹窗的样式与其他弹窗不一致:标题文字紧贴弹窗边框顶部,操作按钮缺少内边距和分隔线。

## 根因分析

`NewToolsModal` 的 DOM 结构与项目其他弹窗不一致:

```tsx
// 修复前(错误结构)
<div className="modal">
  <div className="modal-title">...</div>   // 无 padding,贴顶
  <div className="modal-body">...</div>
  <div className="modal-actions">...</div> // 只有 margin-top,无 padding
</div>
```

- `.modal-title` 本身只有 `font-size` / `font-weight` / `color`,依赖 `.modal-header` 提供 `padding: 16px 20px` 和 `border-bottom`
- `.modal-actions` 只有 `margin-top: 16px`,没有水平和底部内边距,按钮会贴近弹窗边缘
- 缺少 `role="dialog"` 和 `aria-modal="true"` 无障碍属性

## 修复方案

将结构统一为与其他弹窗一致的 `.modal-header` + `.modal-footer` 模式:

```tsx
// 修复后(正确结构)
<div className="modal" role="dialog" aria-modal="true">
  <div className="modal-header">
    <div className="modal-title">...</div>  // padding: 16px 20px + border-bottom
  </div>
  <div className="modal-body">...</div>
  <div className="modal-footer">...</div>   // padding: 16px 20px + border-top
</div>
```

## 修改文件

- `src/components/skills/modals/NewToolsModal.tsx`:补充 `.modal-header` 包裹标题,将 `.modal-actions` 改为 `.modal-footer`,新增 `role`/`aria-modal` 属性


================================================
FILE: docs/releases/v0.4.2/bugfix-root-skill-install-false-exists.md
================================================
# Bugfix:从探索页安装根目录级 Skill 时误报"已存在于 Hub"

## 问题描述

在探索页搜索并点击安装某个 Skill(如 `titanwings/colleague-skill`)时,弹出错误提示:

> 「.」已存在于 Hub,可前往"我的 Skills"中更新。

实际上该 Skill 从未安装过。

## 根因分析

`titanwings/colleague-skill` 是一个**根目录级 Skill**,即 `SKILL.md` 位于仓库根目录。`list_git_skills` 为此类 Skill 返回的候选项中 `subpath = "."`。

`install_git_skill_from_selection` 在推导 `display_name` 时,直接对 subpath 做字符串处理:

```rust
// 修复前
let mut display_name = name.unwrap_or_else(|| {
    subpath
        .rsplit('/')
        .next()
        .map(|s| s.to_string())
        .unwrap_or_else(|| derive_name_from_repo_url(&parsed.clone_url))
});
```

当 `subpath = "."` 时,`rsplit('/').next()` 返回 `"."`,导致:

- `display_name = "."`
- `central_path = central_dir.join(".") = central_dir`(即中央仓库目录本身)
- `central_path.exists()` 永远为 `true`
- 触发错误:`skill already exists in central repo: /path/to/central/.`

前端 `formatErrorMessage` 从路径中提取名称时调用 `.split('/').pop()` 得到 `"."`,最终显示「.」已存在于 Hub。

## 修复方案

当 `subpath == "."` 时,改为从 repo URL 推导初始名称,后续逻辑会继续用 `SKILL.md` 中的名称做最终重命名:

```rust
// 修复后
let mut display_name = name.unwrap_or_else(|| {
    if subpath == "." {
        derive_name_from_repo_url(&parsed.clone_url)
    } else {
        subpath
            .rsplit('/')
            .next()
            .map(|s| s.to_string())
            .unwrap_or_else(|| derive_name_from_repo_url(&parsed.clone_url))
    }
});
```

## 修改文件

- `src-tauri/src/core/installer.rs`:`install_git_skill_from_selection` 函数中对 `subpath == "."` 单独处理,避免用 `"."` 作为 skill 名称


================================================
FILE: docs/releases/v0.4.3/add-copaw-support.md
================================================
# 新增:支持 Copaw 工具

> 贡献者:[@LeonDevLifeLog](https://github.com/LeonDevLifeLog),PR [qufei1993/skills-hub#50](https://github.com/qufei1993/skills-hub/pull/50)

## 更新内容

新增对 [CoPaw](https://github.com/agentscope-ai/CoPaw) 的支持。CoPaw 是 AgentScope 出品的 AI 个人助理,支持多端接入(钉钉、飞书、微信、Discord 等)、技能扩展与多智能体协作。

安装 skill 后,Skills Hub 可自动将其同步到 CoPaw 的本地技能池。

## 技术细节

CoPaw 的技能池路径与大多数工具不同,使用 `skill_pool` 而非 `skills`:

| 字段 | 值 |
|------|-----|
| Tool ID | `copaw` |
| 显示名称 | Copaw |
| 技能目录 | `~/.copaw/skill_pool/` |
| 检测目录 | `~/.copaw/` |

## 修改文件

- `src-tauri/src/core/tool_adapters/mod.rs`:新增 `ToolId::Copaw` 枚举变体及对应的 `ToolAdapter`
- `README.md`:工具支持列表新增 Copaw
- `docs/README.zh.md`:同步更新中文版工具支持列表


================================================
FILE: docs/releases/v0.4.3/bugfix-github-install-and-frontmatter.md
================================================
# Bugfix:优化 GitHub Skill 安装速度并修复多行 Frontmatter 渲染

## 问题 1:从 GitHub 仓库安装 Skill 很慢并最终超时

### 问题描述

从 GitHub 仓库安装某些 Skill 时,安装弹窗会长时间停留在加载状态,最后超时失败。典型表现:

- 弹窗持续显示「正在安装技能...」
- 日志提示正在执行文件/网络操作
- GitHub 网络较慢或仓库文件较多时更容易复现

### 根因分析

对于形如 `https://github.com/owner/repo/tree/branch/path` 的 GitHub 子目录 URL,原逻辑会优先使用 GitHub Contents API 递归下载目录。

该方式的问题是:

- 需要按目录递归请求 GitHub API
- 文件下载是串行 HTTP 请求
- 文件较多时请求数量快速增加
- API 下载失败后还会 fallback 到 `git clone`,导致慢路径被走两遍

因此在网络不稳定或仓库较大时,用户会看到长时间转圈后超时。

### 修复方案

新增 `clone_or_pull_sparse`,对 GitHub 子目录安装优先使用系统 `git` 执行浅克隆 + 稀疏检出:

```bash
git clone --depth 1 --filter=blob:none --sparse --no-tags ...
git sparse-checkout set --no-cone <subpath>
```

这样只检出目标 Skill 子目录,避免下载整个仓库或逐文件调用 GitHub API。

修复后流程:

- 子目录 GitHub URL:优先走 sparse checkout
- sparse checkout 失败:再 fallback 到 GitHub Contents API
- 更新已安装 Skill:如果记录了 `source_subpath`,同样优先走 sparse checkout
- 缓存 key 加入 `subpath`,避免不同子目录复用同一个稀疏工作区造成冲突

### 影响范围

该优化主要改善精确到子目录的安装链接,例如:

```text
https://github.com/anthropics/skills/tree/main/skills/frontend-design
```

如果用户输入的是仓库根 URL,应用仍需要先扫描仓库中的 Skill 候选项,该场景仍可能触发完整浅克隆。

## 问题 2:搜索/手动添加无法处理非标准 Skill 容器目录

### 问题描述

从探索页在线搜索安装 `technical-writer` 时,搜索结果来自 `skills.sh`:

```json
{
  "name": "technical-writer",
  "skillId": "technical-writer",
  "source": "shubhamsaboo/awesome-llm-apps"
}
```

前端只能拿到仓库根地址:

```text
https://github.com/Shubhamsaboo/awesome-llm-apps
```

实际 Skill 位于:

```text
awesome_agent_skills/technical-writer/SKILL.md
```

原扫描逻辑只识别固定目录(如 `skills/*`、`.claude/skills/*`)和根目录直接子目录,无法发现 `awesome_agent_skills/*` 这种容器目录下的 Skill,导致搜索安装失败或回退到错误提示。

手动添加也存在类似问题:如果输入容器目录链接,例如:

```text
https://github.com/Shubhamsaboo/awesome-llm-apps/blob/main/awesome_agent_skills
```

原逻辑会尝试把整个容器目录当成 Skill 安装,导致 Hub 中出现大量子目录而不是一个有效 Skill。

### 根因分析

问题分为两层:

- 搜索结果只提供仓库根 `source`,不包含实际 Skill 文件夹路径
- 后端发现逻辑依赖目录白名单,无法覆盖 `awesome_agent_skills`、`agent-skills`、`custom-agent-skills` 等变体
- 手动 `tree/blob` 子路径绕过候选发现,直接调用安装命令,因此容器目录不会进入 picker
- 安装入口缺少最终校验,子路径存在但不是有效 Skill 目录时也可能被复制进 Hub

### 修复方案

将 Git Skill 发现改为分层模型,避免继续堆具体目录名白名单:

1. 固定目录快速扫描:
   - `skills/*`
   - `skills/.curated/*`
   - `skills/.experimental/*`
   - `skills/.system/*`
   - `.claude/skills/*`
2. 根目录直接 Skill:
   - `repo/my-skill/SKILL.md`
3. 根目录 Skill 容器:
   - 只扫描根目录下名称包含 `skill` 的容器目录的一层子目录
   - 示例:`repo/awesome_agent_skills/technical-writer/SKILL.md`
   - 示例:`repo/custom-agent-skills/python-expert/SKILL.md`

该方案不是全仓库递归扫描,只多扫一层 `*skill*` 容器目录,性能可控。

同时调整手动添加流程:

- `tree/blob` 路径先调用 `list_git_skills_cmd` 做候选发现
- 路径本身是 Skill:返回 1 个候选并自动安装
- 路径是 Skill 容器:列出容器下一层候选,单个自动安装,多个弹 picker
- 完全找不到 Skill:才提示未找到可导入 Skill

并在安装入口增加最终校验:

- 复制源必须是有效 Skill 目录
- 有 `SKILL.md`,或是允许的 `.claude/skills/*` 目录
- 容器目录本身没有 `SKILL.md` 时,拒绝安装并提示粘贴具体 Skill 文件夹链接

另外保留对 `blob/.../SKILL.md` 的规范化:

```text
.../blob/main/awesome_agent_skills/technical-writer/SKILL.md
```

会转换为:

```text
awesome_agent_skills/technical-writer
```

## 问题 3:`description: |` 只渲染出一个 `|`

### 问题描述

部分 `SKILL.md` 使用 YAML block scalar 写多行描述:

```yaml
---
name: technical-writer
description: |
  Creates clear documentation, API references, guides, and
  technical content for developers and users.
author: awesome-llm-apps
---
```

详情页 Frontmatter 表格中只显示 `|`,后面的描述内容没有显示。

### 根因分析

前端详情页的 `parseFrontmatter` 和后端 `parse_skill_md` 都只支持简单的 `key: value` 单行解析。

当遇到 `description: |` 时:

- `description` 被解析成字面量 `|`
- 后续缩进的多行文本没有关联到 `description`
- 后端存入数据库的描述也可能变成 `|`

### 修复方案

前端和后端同时支持 YAML block scalar:

- `description: |`:保留换行,适合多行描述
- `description: >`:折叠为单段文本,适合普通段落

同时调整 Markdown 表格样式:

- `td` 使用 `white-space: pre-wrap` 保留多行文本
- 单元格顶部对齐
- 长文本允许换行,避免撑破布局

## 验证

已完成以下验证:

- `npm run build` 通过
- `cargo test -q` 通过,`79 passed`
- `cargo fmt --all -- --check` 通过
- 新增 Rust 测试覆盖 `description: |` 解析
- 新增 Rust 测试覆盖 `blob/.../SKILL.md` 路径规范化
- 新增 Rust 测试覆盖 `*skill*` 容器目录发现、非 skill 容器跳过、候选去重
- 新增 Rust 测试覆盖容器目录拒绝安装、容器下具体子 Skill 可安装
- 使用真实 GitHub 仓库验证 sparse checkout 可在约 2 秒内检出目标子目录

## 修改文件

- `src-tauri/src/core/git_fetcher.rs`:新增 `clone_or_pull_sparse`
- `src-tauri/src/core/installer.rs`:GitHub 子目录安装和更新优先使用 sparse checkout;补充 block scalar 解析;新增分层 Skill 发现和安装前校验
- `src-tauri/src/core/github_download.rs`:避免将根目录 `.` 走 GitHub Contents API 子目录下载路径
- `src-tauri/src/core/tests/git_fetcher.rs`:新增 sparse checkout 测试
- `src-tauri/src/core/tests/installer.rs`:新增 `description: |`、路径规范化、分层发现、容器目录校验相关测试
- `src/App.tsx`:手动 `tree/blob` 路径改为先发现候选,再自动安装或弹出选择器
- `src/components/skills/SkillDetailView.tsx`:前端 Frontmatter 解析支持 `|` 和 `>`
- `src/App.css`:修复 Frontmatter 表格中多行描述的展示样式


================================================
FILE: docs/releases/v0.5.0/implementation-plan.md
================================================
# 项目级 Skill 同步功能实现计划

## Context

Skills Hub v0.4.x 只有全局同步。Skill 安装到中央仓库 `~/.skillshub/` 后,再同步到各工具的全局 Skills 目录。

v0.5.0 增加项目级同步能力:同一个 Skill 可以选择同步到全局,或同步到一个或多个项目目录下的工具 Skills 目录。

当前实现已经从原始方案做过几轮交互收敛,本计划按最新代码实现记录。

---

## 已确认的产品规则

### 1. 安装流程不变

Skill 仍然只安装到中央仓库:

```text
~/.skillshub/<skill-name>/
```

全局 / 项目只影响同步目标路径。

### 2. Scope 是 Skill 级别设置

每个 Skill 当前只有一个主要 scope:

```text
global | project
```

工具按钮不单独配置 scope,只控制该工具是否参与当前 scope 的同步。

### 3. 切换 scope 后默认同步当前已安装工具

切换范围时,以 `get_tool_status().installed` 返回的当前已安装工具为准:

- 全局 → 项目:同步到所选项目下所有当前已安装工具。
- 项目 → 全局:同步到所有当前已安装工具的全局目录。

不能把系统支持的全部工具展示出来,也不能根据历史同步过的工具推断同步范围。

### 4. 工具按钮样式不区分 scope

工具按钮只表达同步状态:

- active:已同步
- inactive:未同步

项目级状态通过范围徽标表达,例如 `1 个项目`。项目级工具按钮不使用蓝色样式。

### 5. 项目列表是草稿态

同步范围弹窗中的项目目录列表在点击“应用”前都是草稿:

- 添加项目后点取消,不保存、不统计、不同步。
- 删除项目后点取消,不解除同步。
- 只有点击应用后,才提交最终项目列表。

切换到项目范围时,必须至少选择一个项目目录才能应用。

---

## 1. 数据库迁移

**文件**:`src-tauri/src/core/skill_store.rs`

### Schema

`SCHEMA_VERSION` 升级到 4。

`skill_targets` 新增:

```sql
scope TEXT NOT NULL DEFAULT 'global',
project_path TEXT NULL
```

唯一索引:

```sql
CREATE UNIQUE INDEX idx_skill_targets_unique_scope
ON skill_targets(skill_id, tool, scope, COALESCE(project_path, ''));
```

### 迁移规则

V3 → V4 使用重建表迁移:

1. 创建 `skill_targets_new`
2. 复制旧表数据,旧记录统一写为:

   ```text
   scope = 'global'
   project_path = NULL
   ```

3. 删除旧表
4. 重命名新表
5. 创建新的唯一索引

### Rust 结构

`SkillTargetRecord` 增加:

```rust
pub scope: String,
pub project_path: Option<String>,
```

相关方法签名调整:

```rust
get_skill_target(skill_id, tool, scope, project_path)
delete_skill_target(skill_id, tool, scope, project_path)
```

### 兼容性

老用户升级后,既有全局同步记录会保留,并被识别为全局 scope。不会影响原有全局同步状态。

---

## 2. Tool Adapter 扩展

**文件**:`src-tauri/src/core/tool_adapters/mod.rs`

### 新增函数

```rust
resolve_project_path(adapter, project_root)
supports_project_scope(adapter)
project_relative_skills_dir(adapter)
adapters_sharing_project_skills_dir(adapter)
```

### 当前实现规则

- `supports_project_scope()` 当前返回 `true`。
- UI 不根据支持矩阵展示全部工具,只展示当前已安装工具。
- 项目路径不直接复用全局 `relative_skills_dir`,而是先走 `project_relative_skills_dir()` 的显式映射。
- 未显式映射的工具回退到 adapter 自身的 `relative_skills_dir`。

### 关键路径映射

| 工具 | 项目级路径 |
|------|------------|
| Cursor | `.agents/skills` |
| Codex | `.agents/skills` |
| OpenCode | `.agents/skills` |
| Gemini CLI | `.agents/skills` |
| GitHub Copilot | `.agents/skills` |
| Amp | `.agents/skills` |
| Kimi Code CLI | `.agents/skills` |
| Antigravity | `.agents/skills` |
| Cline | `.agents/skills` |
| Claude Code | `.claude/skills` |
| OpenClaw | `skills` |
| Windsurf | `.windsurf/skills` |
| Qwen Code | `.qwen/skills` |

完整映射以 `project_relative_skills_dir()` 为准。

### 共享目录

项目级同步会按项目路径分组:

```rust
adapters_sharing_project_skills_dir(adapter)
```

共享同一目录的工具只写一份文件系统目标,但会为当前已安装的共享工具写入各自的 `skill_targets` 记录。

---

## 3. 后端命令

**文件**:`src-tauri/src/commands/mod.rs`

### DTO

`ToolInfoDto` 增加:

```rust
supports_project_scope: bool
```

`SkillTargetDto` 增加:

```rust
scope: String,
project_path: Option<String>,
```

### `sync_skill_to_tool`

新增可选参数:

```rust
scope: Option<String>,
projectPath: Option<String>,
```

规则:

- `scope` 默认为 `global`。
- 只允许 `global` / `project`。
- `scope = project` 时,`projectPath` 必填,且必须是已存在目录。
- `scope = global` 时继续检查工具是否安装。
- `scope = project` 时使用 `resolve_project_path()` 生成目标目录。
- 如果同 scope / projectPath 下已有有效 target,且目标存在,则幂等返回成功。
- 同步成功后,对共享同一 Skills 目录且当前已安装的工具写入同步记录。

错误前缀沿用:

```text
TARGET_EXISTS|
TOOL_NOT_INSTALLED|
TOOL_NOT_WRITABLE|
```

### `unsync_skill_from_tool`

新增参数同上。

规则:

- 按 `skillId + tool + scope + projectPath` 定位记录。
- 共享目录工具一起更新 DB 记录。
- 文件系统目标只删除一次。
- 全局范围下,如果共享组内没有任何工具已安装,则视为已经无效,直接成功。

### 最近项目

新增命令:

```rust
get_recent_projects()
save_recent_project(projectPath)
```

实现:

- 存储在 settings 表的 `recent_projects_v1`
- JSON 数组
- 新路径插到最前
- 去重
- 最多保留 8 条
- 只在用户点击“应用”提交项目范围后保存

### 命令注册

**文件**:`src-tauri/src/lib.rs`

注册:

```rust
commands::get_recent_projects
commands::save_recent_project
```

---

## 4. 前端类型

**文件**:`src/components/skills/types.ts`

`ManagedSkill.targets` 增加:

```ts
scope: 'global' | 'project' | string
project_path?: string | null
```

`ToolInfoDto` 增加:

```ts
supports_project_scope: boolean
```

---

## 5. 前端 UI

### FilterBar

**文件**:`src/components/skills/FilterBar.tsx`

新增 scope 下拉筛选:

```text
全部 / 全局 / 项目
```

布局要求:

- 靠右,和排序、搜索、刷新同一组。
- 下拉样式参考排序按钮。
- 不显示额外“范围”文案。
- 排序按钮不显示“排序:”前缀。

### SkillCard

**文件**:`src/components/skills/SkillCard.tsx`

新增范围徽标:

```text
全局
N 个项目
```

项目数量从后端真实 `project` target 中统计,不使用弹窗草稿或本地缓存。

工具按钮:

- 只展示当前用户已安装工具。
- active / inactive 样式保持此前一致。
- 不因为项目级 scope 改成蓝色。

### ScopeSyncModal

**文件**:`src/components/skills/modals/ScopeSyncModal.tsx`

新增同步范围弹窗。

文案:

```text
选择这个 Skill 生效的位置。

全局
在所有项目中可用

项目
仅在选择的项目中可用
```

交互:

- radio 切换后直接展示对应内容,不弹额外确认框。
- 项目模式下展示项目目录列表、选择项目目录按钮、最近项目。
- 项目目录列表使用组件内部 `draftProjects`。
- 点击取消丢弃草稿。
- 点击应用调用 `onScopeChange(draftScope, draftProjects)`。
- 项目模式下 `draftProjects.length === 0` 时禁用应用按钮并显示提示。

### App

**文件**:`src/App.tsx`

新增状态:

```ts
scopeFilter
scopeModalSkill
recentProjects
skillScopeState
```

关键逻辑:

- `getSkillScope(skill)` 以后端实际 target 为主,本地缓存只作兜底。
- `getSkillProjects(skill)` 只从后端 project target 统计项目路径。
- `handleScopeChange(nextScope, nextProjects)`:
  - 清理目标 scope 之外的旧 target。
  - 项目 scope 下,同时清理不在最终项目列表中的旧项目 target。
  - 切到项目时,同步到 `nextProjects × installedToolIds`。
  - 切到全局时,同步到 `installedToolIds`。
  - 应用成功后才写入最近项目和本地 scope 缓存。
- `handlePickProject()` 只返回文件夹选择结果,不直接写入 Skill 状态。

---

## 6. i18n

**文件**:`src/i18n/resources.ts`

新增或更新 key:

| Key | EN | ZH |
|-----|----|----|
| `scope.all` | All | 全部 |
| `scope.global` | Global | 全局 |
| `scope.project` | Project | 项目 |
| `scope.globalBadge` | Global | 全局 |
| `scope.projectCount` | `{{count}} projects` | `{{count}} 个项目` |
| `projectSync.title` | Sync Scope | 同步范围 |
| `projectSync.help` | Choose where this Skill is available. | 选择这个 Skill 生效的位置。 |
| `projectSync.globalDesc` | Available in all projects | 在所有项目中可用 |
| `projectSync.projectDesc` | Available only in selected projects | 仅在选择的项目中可用 |
| `projectSync.projectRequired` | Select at least one project directory to apply project scope. | 请至少选择一个项目目录后再应用项目范围。 |

历史确认弹窗相关 key 可保留,但当前交互不再使用。

---

## 7. 关键文件清单

| 文件 | 改动类型 |
|------|----------|
| `src-tauri/src/core/skill_store.rs` | V4 迁移、结构体、查询方法 |
| `src-tauri/src/core/tool_adapters/mod.rs` | 项目路径映射、共享项目目录分组 |
| `src-tauri/src/commands/mod.rs` | 命令参数、DTO、最近项目命令 |
| `src-tauri/src/lib.rs` | 注册新命令 |
| `src/components/skills/types.ts` | DTO 类型更新 |
| `src/components/skills/FilterBar.tsx` | 范围筛选下拉 |
| `src/components/skills/SkillCard.tsx` | 范围徽标、工具展示 |
| `src/components/skills/modals/ScopeSyncModal.tsx` | 新增范围弹窗 |
| `src/App.tsx` | 状态、筛选、切换、同步逻辑 |
| `src/i18n/resources.ts` | 翻译 |
| `src/App.css` | 新增弹窗、范围徽标、筛选样式 |

---

## 8. 验证方式

1. `npm run check` 通过。
2. 旧数据库升级后,旧 `skill_targets` 均为 `scope = global`,`project_path = NULL`。
3. 全局同步一个 Skill,确认工具按钮保持原 active 样式,范围徽标为“全局”。
4. 打开同步范围弹窗,选择“项目”但不选项目时,“应用”不可点。
5. 选择项目后点取消,卡片项目数量不变化,目录不被同步,最近项目不保存。
6. 选择项目后点应用,确认同步到项目目录下当前已安装工具。
7. 再次打开弹窗,删除项目后点取消,原项目同步仍保留。
8. 删除项目后点应用,确认该项目 target 被清理。
9. 项目切回全局后,确认项目 target 被清理,全局 target 写入当前已安装工具。
10. FilterBar 的“全部 / 全局 / 项目”筛选结果正确。


================================================
FILE: docs/releases/v0.5.0/project-scope-design.md
================================================
# 项目级 Skill 同步 — 设计文档

## 背景

Skills Hub v0.4.x 只支持全局同步:Skill 安装到中央仓库后,同步到各工具的全局目录,在所有项目中均可使用。

v0.5.0 新增**项目级同步**:Skill 可以同步到指定项目中的工具目录,使其只在这些项目中生效。

---

## 核心原则

### 安装位置不变

Skill 文件仍然只安装并维护在中央仓库中:

```text
~/.skillshub/<skill-name>/
```

全局 / 项目只决定同步目标,不改变 Hub 中的 Skill 文件。

### 同步范围是 Skill 级别设置

每个 Skill 有一个当前同步范围:

| 范围 | 含义 |
|------|------|
| 全局 | 同步到各工具的全局 Skills 目录 |
| 项目 | 同步到所选项目下各工具的项目级 Skills 目录 |

同步范围不是每个工具单独设置。工具按钮只表示该工具是否参与当前范围的同步。

### 切换范围默认同步当前已安装工具

全局和项目之间切换时,系统会以**当前用户已安装的工具**为准重新同步:

- 全局 → 项目:移除非项目范围的旧同步记录,并将该 Skill 同步到所选项目下所有当前已安装工具。
- 项目 → 全局:移除非全局范围的旧同步记录,并将该 Skill 同步到所有当前已安装工具的全局目录。

这里的“所有工具”不是系统支持的全部工具,而是当前检测到已安装的工具。

---

## 同步路径

### 全局路径

全局同步继续使用各工具 adapter 中已有的全局路径,例如:

```text
~/.claude/skills/<skill-name>
~/.codex/skills/<skill-name>
```

### 项目路径

项目级同步使用独立的项目路径映射。部分工具的项目级路径和全局路径不一致,不能直接复用全局 `relative_skills_dir`。

当前实现中所有工具都允许项目级同步;UI 只展示当前用户已安装的工具。

主要项目级路径如下:

| 工具 | 项目级 Skills 目录 |
|------|--------------------|
| Cursor / Codex / OpenCode / Gemini CLI / GitHub Copilot / Amp / Kimi Code CLI / Antigravity / Cline | `<project>/.agents/skills/` |
| Claude Code | `<project>/.claude/skills/` |
| OpenClaw | `<project>/skills/` |
| Windsurf | `<project>/.windsurf/skills/` |
| Qwen Code | `<project>/.qwen/skills/` |
| OpenHands | `<project>/.openhands/skills/` |
| 其他已映射工具 | 使用 `project_relative_skills_dir()` 中的显式映射 |
| 未显式映射工具 | 回退到该工具的全局 `relative_skills_dir` |

共享同一项目级目录的工具会共用同一个同步目标。例如多个工具都使用 `<project>/.agents/skills/` 时,文件系统只写一份,数据库只为当前已安装且共享该目录的工具记录同步状态。

---

## 同步方式

同步引擎仍沿用现有策略:

```text
symlink -> junction(Windows)-> copy
```

因此文档中“同步目标”不承诺一定是软链接;具体模式由同步引擎决定,并记录在 `skill_targets.mode` 中。

---

## 交互设计

### Skill Card

Skill 卡片 meta 行新增范围徽标:

```text
ux-designer
Expert UX design assistance...
shubhamsaboo/awesome-llm-apps · 10 小时前 · [1 个项目]

[● Cursor] [● Claude Code] [● Codex] [OpenClaw]
```

范围徽标:

| 文案 | 含义 |
|------|------|
| 全局 | 当前 Skill 使用全局同步 |
| N 个项目 | 当前 Skill 使用项目级同步,且已同步到 N 个项目 |

点击范围徽标打开“同步范围”弹窗。

### 工具按钮

工具按钮颜色不区分全局 / 项目,继续沿用原有语义:

| 状态 | 含义 | 样式 |
|------|------|------|
| 已同步 | 该工具已参与当前范围同步 | 原有 active 样式 |
| 未同步 | 该工具未参与当前范围同步 | 原有 inactive 样式 |

全局 / 项目的区别由范围徽标表达,不通过工具按钮颜色表达。

### 同步范围弹窗

弹窗只暴露用户决策需要的信息:

```text
同步范围 · ux-designer

选择这个 Skill 生效的位置。

○ 全局
  在所有项目中可用

● 项目
  仅在选择的项目中可用

项目目录
  /Users/may/Desktop/test/cc-weixin-test    [x]
  [选择项目目录...]

最近使用
  /Users/may/Desktop/test/cursor-browser    [添加]

[取消] [应用]
```

交互规则:

- 切换 radio 后立即展示对应区域,不再弹出额外确认框。
- 选择“项目”时,必须至少选择一个项目目录才能点击“应用”。
- 新增、移除项目目录均为弹窗内草稿状态。
- 点击“取消”不会保存项目列表,不会影响卡片项目数量,也不会触发同步。
- 点击“应用”后才提交范围和项目列表,并执行同步。
- 最近项目只在应用项目范围后保存,最多保留 8 个。

### 筛选栏

范围筛选使用下拉样式,和排序 / 搜索 / 刷新保持同一行布局:

```text
全部 Skills                         [全部 v] [最近更新 ↕] [搜索 skills...] [刷新]
```

筛选项:

| 选项 | 显示内容 |
|------|----------|
| 全部 | 所有 Skill |
| 全局 | 当前范围为全局的 Skill |
| 项目 | 当前范围为项目的 Skill |

筛选在前端完成,不需要新增后端查询接口。

---

## 数据模型

`skill_targets` 增加范围维度:

```text
scope TEXT NOT NULL DEFAULT 'global'
project_path TEXT NULL
```

唯一索引:

```sql
UNIQUE(skill_id, tool, scope, COALESCE(project_path, ''))
```

含义:

- 全局 target:`scope = 'global'`,`project_path = NULL`
- 项目 target:`scope = 'project'`,`project_path = <project root>`

旧数据库升级到 v0.5.0 时,既有同步记录会迁移为:

```text
scope = 'global'
project_path = NULL
```

因此老用户升级后,原有全局同步状态保持不变。

---

## 前后端接口

### `sync_skill_to_tool`

新增可选参数:

```text
scope?: 'global' | 'project'
projectPath?: string
```

规则:

- `scope` 缺省为 `global`,保持向后兼容。
- `scope = project` 时必须传 `projectPath`,且路径必须是已存在目录。
- 全局同步会检查工具是否已安装。
- 项目同步不依赖全局工具安装路径,但最终记录只写入当前已安装工具。
- 同一路径已有有效 target 时视为幂等成功。

### `unsync_skill_from_tool`

同样新增:

```text
scope?: 'global' | 'project'
projectPath?: string
```

规则:

- 按 `skill_id + tool + scope + project_path` 删除目标记录。
- 共享同一 Skills 目录的工具会一起更新数据库状态。
- 文件系统目标只删除一次,避免共享目录重复删除。

### 最近项目

新增命令:

```text
get_recent_projects
save_recent_project(projectPath)
```

最近项目存入 `settings.recent_projects_v1`,用于项目级弹窗快捷添加。

---

## 实现变更概览

| 层 | 文件 | 变更 |
|----|------|------|
| DB | `skill_store.rs` | `skill_targets` 增加 `scope`、`project_path`,V3→V4 重建表迁移 |
| 后端 | `tool_adapters/mod.rs` | 新增项目级路径解析、共享项目目录分组、项目路径映射 |
| 后端 | `commands/mod.rs` | `sync/unsync` 增加 `scope`、`projectPath`;新增最近项目命令 |
| 后端 | `lib.rs` | 注册 `get_recent_projects`、`save_recent_project` |
| 前端 | `types.ts` | DTO 增加 `scope`、`project_path`、`supports_project_scope` |
| 前端 | `FilterBar.tsx` | 新增范围下拉筛选 |
| 前端 | `SkillCard.tsx` | meta 行增加范围徽标;工具按钮保持原有 active/inactive 样式 |
| 前端 | `ScopeSyncModal.tsx` | 新建同步范围弹窗,使用草稿项目列表,应用后提交 |
| 前端 | `App.tsx` | 新增范围状态、筛选、切换、项目同步逻辑 |
| i18n | `resources.ts` | 新增 `scope.*`、`projectSync.*` 翻译键 |

详细实现步骤见:[实现计划](./implementation-plan.md)


================================================
FILE: docs/releases/v0.5.0/ux-optimizations.md
================================================
# UX 优化记录

收录不需要单独文档的小型 UX 改进。

---

## 关闭按钮改为隐藏窗口(macOS)

**变更:** 点击红色 X 按钮不再退出应用,而是隐藏窗口。

**原因:** macOS 上许多主流应用(Slack、Discord 等)均采用此交互模式——应用在后台持续运行,下次打开时响应更快。需要真正退出时使用 `Cmd+Q` 或菜单栏退出。

**实现方式:**
- 拦截 `CloseRequested` 窗口事件,阻止默认关闭行为,改为隐藏窗口。
- 点击 Dock 图标时触发 `RunEvent::Reopen`,重新显示窗口并聚焦。

**涉及文件:** `src-tauri/src/lib.rs`

---

## Skill 描述与 Markdown 预览优化

**变更:** 修复 `SKILL.md` frontmatter 在列表和详情页中的展示问题。

**原因:** 部分 Skill 使用 YAML 折叠块语法(例如 `description: >-`)。旧解析逻辑没有识别 `>-`、`>+`、`|-`、`|+`,导致列表卡片错误显示 `>-`,详情页元信息也可能出现描述缺失或排版异常。

**实现方式:**
- 后端 `SKILL.md` 解析支持 YAML block scalar 的 chomping indicator:`>`、`>-`、`>+`、`|`、`|-`、`|+`。
- 启动时重新从 `SKILL.md` 比对并回填描述,纠正已入库的旧错误值。
- 详情页 frontmatter 改为响应式 key/value 元信息区,避免短字段被表格挤压成竖排。
- Markdown 预览内容区居中展示,并保留最大可读宽度。

**涉及文件:**
- `src-tauri/src/core/installer.rs`
- `src-tauri/src/core/skill_store.rs`
- `src-tauri/src/core/tests/installer.rs`
- `src-tauri/src/core/tests/skill_store.rs`
- `src/components/skills/SkillDetailView.tsx`
- `src/App.css`

---

## 取消同步后的 Agent 重新启用入口

**变更:** 修复在“我的 skills”页面取消部分 agent 同步后,已取消同步的 agent 没有重新启用入口的问题。

**原因:** v0.4.3 中未同步 agent 只会在卡片展开状态下渲染。触发场景是:某个 skill 同步到多个 agent 后,用户取消 Qoder、GitHub Copilot 等部分 agent;如果该 skill 剩余已同步 agent 数量不超过 5 个,卡片不会显示 `+N more` 展开按钮,导致被取消同步的 agent 无法以灰色按钮显示,看起来像“消失了,无法重新添加”。关闭重开应用也不会恢复,因为这是渲染条件问题,不是临时状态卡住。

**实现方式:**
- 当卡片不需要折叠时,直接显示未同步 agent 的灰色按钮。
- 保留折叠场景下通过 `+N more` 展开查看完整 agent 列表的行为。

**涉及文件:** `src/components/skills/SkillCard.tsx`

---

## Bug:导入同名且内容一致的 Skill 时同步冲突

**关联 Issue:** https://github.com/qufei1993/skills-hub/issues/51

**状态:** 已在本地验证修复,待 v0.5.0 发版后关闭 issue。

**变更:** 修复从一个工具导入 Skill 后,同名且内容一致的其它工具目录仍被判定为同步冲突的问题;同时修复导入弹窗在部分同步失败后不关闭、未选择任何 Skill 时仍可点击“导入并同步”的问题。

**原因:** 旧同步逻辑只判断目标目录是否存在。即使 Codex、Cursor 等工具下的同名 Skill 内容完全一致,也会返回 `TARGET_EXISTS`,导致该工具永远无法被 Hub 接管。导入流程还会在部分失败时只显示错误 toast,不关闭 `ImportModal`。

**实现方式:**
- `sync_skill_to_tool` 增加 `overwriteIfSameContent` 参数。
- 后端在目标目录已存在时比较 Hub 中央仓库 Skill 与目标目录的内容 hash;内容一致时允许安全接管,内容不一致时继续阻止覆盖。
- 所有前端同步入口都传入 `overwriteIfSameContent: true`,确保导入、创建后同步、单个工具同步、批量同步和范围切换行为一致。
- 导入流程改为逐项处理,单个 Skill 导入或同步失败不会中断后续项。
- 导入结束后统一关闭弹窗;有错误时只显示错误提示,全部成功时才显示成功提示。
- 未选择任何 Skill 时禁用“导入并同步”,并在提交入口增加空选择校验。

**涉及文件:**
- `src-tauri/src/commands/mod.rs`
- `src/App.tsx`
- `src/components/skills/modals/ImportModal.tsx`

---

## 新增:Hermes Agent 全局同步支持

**关联 Issue:** https://github.com/qufei1993/skills-hub/issues/54

**状态:** 已在本地验证,纳入 v0.5.0。

**变更:** 新增 Hermes Agent 工具适配,支持将 Skill 全局同步到 `~/.hermes/skills`。

**原因:** Issue #54 请求支持 Hermes Agent。调研 Hermes Agent 官方文档后,仅确认默认 `HERMES_HOME` 为 `~/.hermes`,Skills 位于 `HERMES_HOME/skills`。没有找到明确的项目级 skills 目录规范,因此本次只声明并实现全局同步支持,避免为项目级同步捏造路径。

**实现方式:**
- 新增 `hermes_agent` 工具 key,展示名为 `Hermes Agent`。
- 全局 skills 目录配置为 `.hermes/skills`,安装检测目录为 `.hermes`。
- `supports_project_scope` 对 Hermes Agent 返回 `false`。
- 后端在项目级同步入口拒绝不支持项目级同步的工具,返回 `PROJECT_SCOPE_UNSUPPORTED|<tool>`。
- 前端在项目级批量同步时跳过 Hermes Agent;用户在项目级 Skill 上单独点击 Hermes Agent 时显示“不支持项目级同步”提示。
- README 和 CHANGELOG 同步标注 Hermes Agent 仅支持全局同步,并列入 v0.5.0。

**涉及文件:**
- `src-tauri/src/core/tool_adapters/mod.rs`
- `src-tauri/src/commands/mod.rs`
- `src-tauri/src/core/tests/tool_adapters.rs`
- `src/App.tsx`
- `src/i18n/resources.ts`
- `README.md`
- `docs/README.zh.md`
- `CHANGELOG.md`
- `docs/CHANGELOG.zh.md`


================================================
FILE: docs/releases/v0.6.0/minor-updates.md
================================================
# v0.6.0 小需求与体验优化记录

这个文件用于记录 v0.6.0 周期内较小的需求、体验优化和界面修正。后续同类变更继续追加到这里,避免为每个小项单独创建发布记录文件。

## 2026-05-05

### 删除 My Skills 筛选栏刷新按钮

- 删除 My Skills 筛选栏中的 `Refresh` / `刷新` 按钮,解决中文界面下按钮样式错乱问题(GitHub Issue #61,PR #63)。
- 确认该按钮仅手动重新读取当前 Skill 列表和标签,不触发重新扫描工具、Git 更新或重新同步。
- 安装、删除、同步、编辑标签等操作后已有自动刷新,不影响现有数据更新流程。
- 修复验证(PR #63):`npm run check`。

### 查看并导入 Skill 增加搜索栏

- 在“查看并导入 Skill”弹窗中新增搜索栏,支持按名称、描述和路径筛选候选项(GitHub Issue #57)。
- 搜索结果会同步影响“全选”和已选数量统计,方便在多个 Skill 中快速定位目标。
- 本地目录导入和 Git 仓库导入两个入口都已支持该交互。
- “查看发现的 Skills”弹窗也已补上搜索栏,支持按 Skill 名称或路径筛选已发现条目。
- 修复验证:`npm run lint`、`npx tsc -b`、`npm run rust:fmt:check`、`npm run rust:clippy`、`npm run rust:test`。


================================================
FILE: docs/releases/v0.6.0/tag-management.md
================================================
# Skill 标签管理功能设计与实现计划

## 背景

随着用户安装的 Skill 数量增加,当前列表主要依赖名称、描述和同步状态浏览。用户很难按技术方向或使用场景快速定位 Skill,例如:

- 前端 / React / UI
- Rust / Tauri
- 文档 / 图表 / 自动化
- 测试 / 代码审查

v0.6.0 聚焦解决这个问题:为 Skill 增加**自定义标签**能力,用于整理和筛选 Skill。

关联需求:

- GitHub Issue #15:添加 tag 功能。

---

## 本期目标

v0.6.0 只实现标签功能。

标签用于:

- 给 Skill 添加一个或多个自定义标签。
- 在 My Skills 页面按标签筛选 Skill。
- 找出还没有设置标签的 Skill。
- 通过独立的 Tags 页面查看和编辑标签。

标签不用于:

- 控制 Skill 是否同步。
- 控制 Skill 同步到哪些工具。
- 作为一套可切换的工作配置。

一句话定义:

```text
Tag 用于找 Skill,不用于改变 Skill 的生效范围。
```

---

## 产品规则

### 1. Skill 可以没有标签

没有标签是合法状态。

系统提供虚拟筛选项:

```text
Untagged
```

含义:

```text
显示没有任何标签的 Skill。
```

`Untagged` 不是用户创建的真实标签,不能重命名、删除,也不写入标签表。

### 2. Skill 可以有多个标签

一个 Skill 可以关联多个标签:

```text
frontend-design: Frontend, Docs, UI
```

同一个 Skill 下不能重复添加同一个标签。

### 3. 标签筛选支持多选

My Skills 页面提供 `Tags` 下拉筛选器:

```text
[All ▾] [Most recent ⇅] [Tags ▾] [Search skills...] [Refresh]
```

下拉内容:

```text
Tags                              Match any

[Search tags...]

[ ] Untagged                  5
[✓] Frontend                  8
[✓] React                     3
[ ] Rust                      2
[ ] Docs                      6

Clear all
```

筛选逻辑使用 `OR`:

```text
选择 Frontend + Docs
```

显示包含 `Frontend` 或 `Docs` 的 Skill。

### 4. 筛选即时生效

标签筛选只影响列表展示,不修改数据,因此不需要 `Apply`。

交互规则:

- 勾选标签后,Skill 列表立即更新。
- 取消勾选后,Skill 列表立即更新。
- 下拉保持打开,方便连续选择。
- 点击外部关闭下拉。
- `Clear all` 立即清空标签筛选。

### 5. 标签页面是独立入口

标签不放在 Settings 中,避免入口过深。

入口:

- 顶部主导航的 `Tags` / `标签`
- My Skills 页面标签筛选区的辅助入口(保留一个快速跳转点)

页面层级:

```text
Tags
```

页面内容:

```text
Tags

标签用于筛选和整理 skills,不会改变同步结果。

5 skills have no tags                         [Review]

[Search tags...]                              [+ New Tag]

Tag name        Skills      Last used       Actions
Frontend        8           2d ago          View  Rename  Delete
Docs            6           5d ago          View  Rename  Delete
Rust            2           9d ago          View  Rename  Delete
```

`Review` 点击后回到 My Skills,并应用 `Untagged` 筛选。

`View` 点击后回到 My Skills,并应用对应标签筛选。

---

## UI 行为

### Skill 卡片

Skill 卡片展示最多 2-3 个标签,避免挤压工具同步状态。

有标签:

```text
frontend-design    #Frontend #Docs
```

无标签:

```text
legacy-shell-helper
```

没有标签时不展示空文案,只保留标签编辑入口。

### 标签编辑

每个 Skill 需要有入口编辑标签。

当前实现使用 Skill 卡片右侧的标签图标按钮作为快捷入口,点击后打开标签编辑。

编辑内容:

```text
Edit Tags: frontend-design

[✓] Frontend
[✓] Docs
[ ] Rust
[ ] Testing

[Done]
```

### 新建标签

新建标签入口在 `Tags` 页面。

规则:

- 标签名不能为空。
- 标签名去除首尾空格。
- 标签名大小写不敏感去重。
- 新建后出现在标签管理表和筛选下拉中。

### 重命名标签

重命名标签会更新所有关联 Skill 的标签展示。

规则:

- 不能重命名为已存在标签。
- 重命名后保留原有关联关系。
- 如果当前正在按旧标签筛选,筛选条件同步切换为新标签名。

### 删除标签

删除标签只删除标签和关联关系,不删除 Skill。

删除前需要确认:

```text
Delete "Docs" from 6 skills?
This only removes the tag, not the skills.
```

删除后:

- 标签从所有 Skill 上移除。
- 如果某些 Skill 因此没有任何标签,它们会进入 `Untagged`。

---

## 数据模型

新增两张表:

```sql
CREATE TABLE skill_tags (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL UNIQUE,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL
);

CREATE TABLE skill_tag_links (
  skill_id INTEGER NOT NULL,
  tag_id INTEGER NOT NULL,
  created_at TEXT NOT NULL,
  PRIMARY KEY (skill_id, tag_id),
  FOREIGN KEY (skill_id) REFERENCES skills(id) ON DELETE CASCADE,
  FOREIGN KEY (tag_id) REFERENCES skill_tags(id) ON DELETE CASCADE
);
```

约束:

- `skill_tags.name` 唯一。
- `skill_tag_links` 使用 `(skill_id, tag_id)` 主键,防止同一 Skill 重复关联同一标签。
- `Untagged` 不入库,由 `NOT EXISTS skill_tag_links` 动态计算。

迁移:

- 升级数据库 schema 版本。
- 老用户升级后,既有 Skill 默认没有标签。
- 不自动生成标签,避免误分类。

---

## 后端能力

建议新增 core 方法:

```rust
create_tag(name)
rename_tag(tag_id, name)
delete_tag(tag_id)
list_tags_with_counts()
set_skill_tags(skill_id, tag_ids)
get_skill_tags(skill_id)
list_untagged_skill_ids()
```

建议新增 Tauri commands:

```text
get_tags
create_tag
rename_tag
delete_tag
set_skill_tags
get_skill_tags
```

`get_managed_skills` 返回的 Skill DTO 增加:

```ts
tags: TagDto[]
```

`TagDto`:

```ts
type TagDto = {
  id: number;
  name: string;
};
```

标签筛选第一版可在前端完成,不需要新增服务端筛选参数。

---

## 前端改动

主要涉及:

- `src/App.tsx`
- `src/components/skills/types.ts`
- `src/components/skills/FilterBar.tsx`
- `src/components/skills/SkillCard.tsx`
- 新增 Tags 页面组件
- 新增标签编辑弹窗或详情页 Tags 区块
- `src/i18n/resources.ts`
- `src/App.css`

### 状态

新增状态:

```ts
tags
selectedTagIds
tagSearch
tagManagerSearch
tagEditorSkill
```

### 筛选

前端筛选逻辑:

```text
selectedTags 为空 -> 显示全部
selectedTags 包含普通标签 -> 命中任意一个标签
selectedTags 包含 Untagged -> skill.tags.length === 0
```

### i18n

所有用户可见文案需要提供英文和中文:

- Tags
- Untagged
- New Tag
- Rename
- Delete
- Review
- Clear all
- Match any

---

## 测试重点

### 后端

- 新建标签成功。
- 重复标签名被拒绝。
- 重命名标签保留关联关系。
- 删除标签后关联关系清理。
- 同一 Skill 不能重复关联同一标签。
- 删除 Skill 后标签关联被清理。
- `Untagged` 统计正确。

### 前端

- 多选标签即时筛选。
- `Untagged` 筛选正确。
- `Clear all` 清空筛选。
- 顶部 `Tags` 进入标签页面。
- Tags 页面 `Review` 能筛选无标签 Skill。
- Tags 页面 `View` 能筛选指定标签。
- 无标签 Skill 不显示空文案。
- 编辑标签后卡片和筛选结果刷新。
- 在添加 Skill 弹窗中可直接选择标签,创建后自动绑定。

---

## 发布范围

v0.6.0 包含:

- 自定义标签数据模型。
- 标签 CRUD。
- Skill 标签关联编辑。
- My Skills 标签多选筛选。
- `Untagged` 虚拟筛选项。
- 独立 Tags 页面。
- 添加 Skill 时选择标签。
- 中英文文案。
- 后端和前端测试。

v0.6.0 不包含:

- 标签自动推荐。
- 批量给多个 Skill 添加标签。


================================================
FILE: docs/skills_hub_design.html
================================================
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Skills Hub - Design Preview (Optimized Light Mode)</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Fira+Sans:wght@300;400;500;600;700&family=Fira+Code:wght@400;500;600;700&display=swap" rel="stylesheet">
    <style>
        :root {
            /* Colors - Clean Light Theme */
            --bg-app: #ffffff;
            --bg-panel: #fcfcfc;
            --bg-element: #f4f4f5;
            --bg-element-hover: #e4e4e7;
            
            --border-subtle: #e4e4e7;
            --border-strong: #d4d4d8;
            
            --text-primary: #18181b;
            --text-secondary: #52525b;
            --text-tertiary: #a1a1aa;
            
            /* Executive Dashboard Accent Palette */
            --accent-primary: #2563EB; /* Slightly darker blue for white bg */
            --accent-primary-hover: #1D4ED8;
            --accent-primary-fg: #FFFFFF;
            
            --status-success: #059669;
            --status-warning: #d97706;
            --status-error: #dc2626;
            --status-info: #2563EB;
            
            /* Typography */
            --font-ui: 'Fira Sans', system-ui, -apple-system, sans-serif;
            --font-mono: 'Fira Code', monospace;
            
            /* Spacing & Layout */
            --radius-sm: 4px;
            --radius-md: 8px;
            --radius-lg: 12px;
            
            /* Effects */
            --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
            --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.05);
            --glow-text: none;
        }

        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }

        body {
            background-color: #e5e5e5;
            color: var(--text-primary);
            font-family: var(--font-ui);
            padding: 40px;
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 60px;
            min-height: 100vh;
            -webkit-font-smoothing: antialiased;
        }

        /* Presentation Label */
        .design-label {
            position: absolute;
            top: -30px;
            left: 0;
            font-family: var(--font-mono);
            font-size: 12px;
            color: var(--text-secondary);
            text-transform: uppercase;
            letter-spacing: 0.1em;
            text-shadow: none;
        }

        /* App Window Frame Container */
        .window-frame {
            position: relative;
            width: 1000px; /* Slightly wider for data density */
            height: 700px;
            background: var(--bg-app);
            border: 1px solid var(--border-subtle);
            border-radius: var(--radius-lg);
            box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.15);
            display: flex;
            flex-direction: column;
            overflow: hidden;
        }

        /* Title Bar */
        .title-bar {
            height: 40px;
            background: var(--bg-app);
            border-bottom: 1px solid var(--border-subtle);
            display: flex;
            align-items: center;
            padding: 0 16px;
            -webkit-app-region: drag;
        }

        .traffic-lights {
            display: flex;
            gap: 8px;
        }

        .traffic-light {
            width: 12px;
            height: 12px;
            border-radius: 50%;
        }
        .traffic-light.close { background: #ff5f56; border: 1px solid rgba(0,0,0,0.1); }
        .traffic-light.minimize { background: #ffbd2e; border: 1px solid rgba(0,0,0,0.1); }
        .traffic-light.maximize { background: #27c93f; border: 1px solid rgba(0,0,0,0.1); }

        .app-layout {
            display: flex;
            flex-direction: column;
            flex: 1;
            overflow: hidden;
        }

        /* Top Navigation Header */
        .app-header {
            height: 64px;
            padding: 0 24px;
            display: flex;
            align-items: center;
            justify-content: space-between;
            border-bottom: 1px solid var(--border-subtle);
            background: rgba(255, 255, 255, 0.8); /* Glass effect base */
            backdrop-filter: blur(12px);
            -webkit-backdrop-filter: blur(12px);
            z-index: 10;
        }

        .brand-area {
            display: flex;
            align-items: center;
            gap: 12px;
        }

        .logo-icon {
            width: 36px;
            height: 36px;
            display: block;
            object-fit: contain;
            border-radius: 8px;
        }
        
        .brand-text {
            font-weight: 700;
            font-size: 20px;
            letter-spacing: -0.03em;
            /* UI UX Pro Max Gradient: Blue -> Purple -> Orange */
            background: linear-gradient(to right, #2563EB, #9333EA, #EA580C);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
        }

        .header-actions {
            display: flex;
            align-items: center;
            gap: 16px;
        }

        /* Optimized Language Switcher */
        .lang-btn {
            font-size: 12px;
            font-weight: 600;
            color: var(--text-secondary);
            cursor: pointer;
            padding: 6px 10px;
            border-radius: var(--radius-sm);
            background: transparent;
            transition: all 0.2s;
            border: 1px solid var(--border-subtle);
            font-family: var(--font-mono);
            display: flex;
            align-items: center;
            gap: 6px;
        }
        .lang-btn:hover {
            color: var(--text-primary);
            background: var(--bg-element);
            border-color: var(--text-tertiary);
        }

        .icon-btn-header {
            width: 36px;
            height: 36px;
            display: flex;
            align-items: center;
            justify-content: center;
            border-radius: var(--radius-md);
            color: var(--text-secondary);
            cursor: pointer;
            transition: all 0.2s;
            overflow: visible;
        }
        .icon-btn-header:hover {
            background: var(--bg-element);
            color: var(--text-primary);
            transform: translateY(-1px);
        }
        
        .icon-btn-header svg {
            width: 20px;
            height: 20px;
        }

        .btn {
            height: 36px;
            padding: 0 16px;
            border-radius: var(--radius-md);
            font-size: 13px;
            font-weight: 600;
            cursor: pointer;
            display: flex;
            align-items: center;
            gap: 8px;
            border: 1px solid transparent;
            transition: all 0.2s;
            position: relative;
            overflow: hidden;
        }

        .btn-primary {
            background: var(--accent-primary);
            color: var(--accent-primary-fg);
            box-shadow: 0 4px 6px -1px rgba(37, 99, 235, 0.2);
        }
        .btn-primary:hover {
            background: var(--accent-primary-hover);
            transform: translateY(-1px);
            box-shadow: 0 6px 8px -1px rgba(37, 99, 235, 0.3);
        }
        .btn-primary:active {
            transform: translateY(0);
        }

        .btn-secondary {
            background: transparent;
            border-color: var(--border-subtle);
            color: var(--text-primary);
        }
        .btn-secondary:hover {
            border-color: var(--border-strong);
            background: var(--bg-element);
        }
        
        .btn-danger {
            background: #fef2f2;
            color: var(--status-error);
            border: 1px solid #fee2e2;
        }
        .btn-danger:hover {
            background: #fee2e2;
            border-color: var(--status-error);
        }
        .btn-danger-solid {
            background: var(--status-error);
            color: #fff;
            box-shadow: 0 4px 6px -1px rgba(220, 38, 38, 0.2);
        }
        .btn-danger-solid:hover {
            opacity: 0.9;
            transform: translateY(-1px);
        }

        /* Stats Grid (New) */
        .stats-grid {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 16px;
            padding: 24px 32px 0 32px;
        }

        .stat-card {
            background: var(--bg-panel);
            border: 1px solid var(--border-subtle);
            border-radius: var(--radius-md);
            padding: 16px;
            display: flex;
            flex-direction: column;
            gap: 4px;
            transition: all 0.2s;
        }
        .stat-card:hover {
            border-color: var(--border-strong);
            transform: translateY(-1px);
        }

        .stat-label {
            font-size: 12px;
            color: var(--text-tertiary);
            font-weight: 500;
            text-transform: uppercase;
            letter-spacing: 0.05em;
        }
        .stat-value {
            font-size: 24px;
            font-weight: 600;
            color: var(--text-primary);
            letter-spacing: -0.02em;
        }

        /* Sub-Header / Filter Bar */
        .filter-bar {
            padding: 20px 32px 0 32px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .filter-title {
            font-size: 14px;
            font-weight: 500;
            color: var(--text-secondary);
            font-family: var(--font-mono);
        }

        .search-container {
            position: relative;
        }

        .search-input {
            background: var(--bg-panel);
            border: 1px solid var(--border-subtle);
            border-radius: var(--radius-md);
            height: 36px;
            padding: 0 12px 0 36px;
            color: var(--text-primary);
            width: 280px;
            font-size: 13px;
            transition: all 0.2s;
            font-family: var(--font-ui);
        }
        .search-input:focus {
            outline: none;
            border-color: var(--accent-primary);
            background: var(--bg-app);
            box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1);
        }

        .search-icon-abs {
            position: absolute;
            left: 12px;
            top: 50%;
            transform: translateY(-50%);
            width: 16px;
            height: 16px;
            color: var(--text-tertiary);
            pointer-events: none;
        }


        /* Main Content */
        .main-content {
            flex: 1;
            display: flex;
            flex-direction: column;
            background: var(--bg-app);
            overflow: hidden;
        }

        /* Skills List - Data Dense */
        .skills-list {
            padding: 16px 32px 32px 32px;
            display: flex;
            flex-direction: column;
            gap: 12px;
            overflow-y: auto;
        }

        .skill-card {
            background: var(--bg-app);
            border: 1px solid var(--border-subtle);
            border-radius: var(--radius-lg);
            padding: 16px 20px;
            display: grid;
            grid-template-columns: 48px 2fr 1.5fr auto;
            gap: 20px;
            align-items: center;
            transition: all 0.2s;
            position: relative;
        }

        .skill-card:hover {
            border-color: var(--border-strong);
            background: var(--bg-panel);
            transform: translateY(-1px);
            box-shadow: var(--shadow-sm);
            z-index: 1;
        }

        .skill-icon {
            width: 48px;
            height: 48px;
            background: var(--bg-element);
            border: 1px solid var(--border-subtle);
            border-radius: var(--radius-md);
            display: flex;
            align-items: center;
            justify-content: center;
            color: var(--text-secondary);
            font-size: 20px;
            transition: all 0.2s;
        }
        .skill-card:hover .skill-icon {
            color: var(--text-primary);
            border-color: var(--border-strong);
            background: #fff;
        }

        .skill-main {
            display: flex;
            flex-direction: column;
            gap: 4px;
            justify-content: center;
        }

        .skill-header-row {
            display: flex;
            align-items: center;
            gap: 10px;
        }

        .skill-name {
            font-weight: 600;
            color: var(--text-primary);
            font-size: 15px;
            letter-spacing: -0.01em;
        }

        .skill-version-badge {
            font-family: var(--font-mono);
            font-size: 10px;
            color: var(--text-secondary);
            background: var(--bg-element);
            padding: 2px 6px;
            border-radius: 4px;
            border: 1px solid var(--border-subtle);
        }

        .skill-meta-row {
            display: flex;
            align-items: center;
            gap: 12px;
            margin-top: 4px;
        }

        .skill-source {
            font-family: var(--font-mono);
            font-size: 11px;
            color: var(--text-tertiary);
            display: flex;
            align-items: center;
            gap: 6px;
        }

        .skill-status-col {
            display: flex;
            flex-direction: column;
            gap: 6px;
            justify-content: center;
        }
        
        .status-label {
            font-size: 11px;
            font-weight: 500;
            color: var(--text-tertiary);
            text-transform: uppercase;
            letter-spacing: 0.05em;
        }

        /* Discovery Styles */
        .discovery-header {
            display: flex;
            align-items: center;
            gap: 8px;
            margin-bottom: 8px;
            padding: 0 4px;
        }
        .discovery-title {
            font-size: 12px;
            font-weight: 600;
            color: var(--text-secondary);
            text-transform: uppercase;
            letter-spacing: 0.05em;
        }
        .discovery-count {
            background: var(--status-warning);
            color: white;
            font-size: 10px;
            font-weight: 700;
            padding: 1px 6px;
            border-radius: 10px;
        }
        .skill-card.discovery {
            background: #FFFBEB; /* Light yellow tint for discovery */
            border-style: dashed;
            border-color: #FCD34D;
        }
        .skill-card.discovery:hover {
            border-color: var(--status-warning);
            background: #FEF3C7;
            border-style: solid;
        }

        /* Tool Matrix - Optimized for Density */
        .tool-matrix {
            display: flex;
            align-items: center;
            gap: 6px;
            flex-wrap: wrap;
        }

        .tool-pill {
            height: 22px;
            padding: 0 8px;
            border-radius: 4px;
            font-size: 11px;
            font-weight: 500;
            display: flex;
            align-items: center;
            gap: 6px;
            cursor: pointer;
            transition: all 0.2s;
            border: 1px solid var(--border-subtle);
            font-family: var(--font-ui);
            background: #fff;
            color: var(--text-secondary);
        }

        /* Active Tool State - High Contrast */
        .tool-pill.active {
            background: #fff;
            color: var(--status-success);
            border-color: #d1fae5;
        }
        .tool-pill.active .status-badge {
            background: var(--status-success);
            box-shadow: 0 0 0 2px #d1fae5;
        }
        
        .tool-pill.active:hover {
            border-color: var(--status-success);
            transform: translateY(-1px);
        }

        /* Inactive Tool State */
        .tool-pill.inactive {
            background: var(--bg-element);
            color: var(--text-tertiary);
            border-color: transparent;
        }

        .tool-pill.inactive:hover {
            color: var(--text-secondary);
            background: var(--bg-element-hover);
        }
        
        /* Not Installed / Unknown State */
        .tool-pill.disabled {
            opacity: 0.5;
            cursor: not-allowed;
            border: 1px dashed var(--border-strong);
            background: transparent;
            color: var(--text-tertiary);
        }

        .status-badge {
            width: 6px;
            height: 6px;
            border-radius: 50%;
            background: var(--text-tertiary);
        }

        .skill-actions-col {
            display: flex;
            flex-direction: row;
            align-items: center;
            gap: 8px;
            align-self: center;
        }

        /* Card Action Buttons */
        .card-btn {
            height: 32px;
            width: 32px;
            padding: 0;
            border-radius: var(--radius-md);
            background: transparent;
            color: var(--text-tertiary);
            border: 1px solid transparent;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 8px;
            transition: all 0.2s;
        }
        .card-btn:hover {
            color: var(--text-primary);
            background: var(--bg-element);
            border-color: var(--border-subtle);
            transform: translateY(-1px);
            box-shadow: var(--shadow-sm);
        }
        .card-btn.primary-action {
            color: var(--accent-primary);
            background: #eff6ff;
            border-color: #dbeafe;
        }
        .card-btn.primary-action:hover {
            background: var(--accent-primary);
            color: #fff;
            border-color: var(--accent-primary);
        }
        .card-btn.danger-action:hover {
            color: var(--status-error);
            background: #fef2f2;
            border-color: #fee2e2;
        }


        /* Modal Overlay */
        .modal-showcase {
            display: flex;
            gap: 40px;
            justify-content: center;
            flex-wrap: wrap;
            wi
Download .txt
gitextract_nr53l0gg/

├── .github/
│   └── workflows/
│       ├── ci.yml
│       ├── release.yml
│       └── update-featured-skills.yml
├── .gitignore
├── AGENTS.md
├── CHANGELOG.md
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── docs/
│   ├── CHANGELOG.zh.md
│   ├── README.zh.md
│   ├── future/
│   │   └── profile-requirements.md
│   ├── releases/
│   │   ├── v0.1-v0.2/
│   │   │   ├── system-design.md
│   │   │   └── system-design.zh.md
│   │   ├── v0.3.0/
│   │   │   ├── plan-bug-fixes.md
│   │   │   ├── plan-explore-page-redesign.md
│   │   │   ├── plan-featured-skills.md
│   │   │   ├── plan-online-search.md
│   │   │   └── plan-skill-detail-view.md
│   │   ├── v0.3.1/
│   │   │   ├── plan-in-app-update.md
│   │   │   ├── plan-qoderwork-support.md
│   │   │   ├── plan-settings-page.md
│   │   │   └── skills-aggregation-repo.md
│   │   ├── v0.4.0/
│   │   │   ├── bugfix-language-toggle-loading-overlay.md
│   │   │   ├── plan-in-app-update.md
│   │   │   ├── plan-qoderwork-support.md
│   │   │   ├── plan-settings-page.md
│   │   │   └── skills-aggregation-repo.md
│   │   ├── v0.4.1/
│   │   │   └── plan-frontmatter-table.md
│   │   ├── v0.4.2/
│   │   │   ├── bugfix-new-tools-modal-style.md
│   │   │   └── bugfix-root-skill-install-false-exists.md
│   │   ├── v0.4.3/
│   │   │   ├── add-copaw-support.md
│   │   │   └── bugfix-github-install-and-frontmatter.md
│   │   ├── v0.5.0/
│   │   │   ├── implementation-plan.md
│   │   │   ├── project-scope-design.md
│   │   │   └── ux-optimizations.md
│   │   └── v0.6.0/
│   │       ├── minor-updates.md
│   │       └── tag-management.md
│   ├── skills_hub_design.html
│   ├── skills_hub_v2_design.html
│   └── tag_profile_interactive_prototype.html
├── eslint.config.js
├── featured-skills.json
├── index.html
├── package.json
├── scripts/
│   ├── coverage-rust.sh
│   ├── extract-changelog.mjs
│   ├── fetch-featured-skills.mjs
│   ├── tauri-icon-desktop.mjs
│   └── version.mjs
├── src/
│   ├── App.css
│   ├── App.tsx
│   ├── components/
│   │   ├── Layout.tsx
│   │   └── skills/
│   │       ├── ExplorePage.tsx
│   │       ├── FilterBar.tsx
│   │       ├── Header.tsx
│   │       ├── LoadingOverlay.tsx
│   │       ├── SettingsPage.tsx
│   │       ├── SkillCard.tsx
│   │       ├── SkillDetailView.tsx
│   │       ├── SkillsList.tsx
│   │       ├── TagsPage.tsx
│   │       ├── modals/
│   │       │   ├── AddSkillModal.tsx
│   │       │   ├── DeleteModal.tsx
│   │       │   ├── EditSkillTagsModal.tsx
│   │       │   ├── GitPickModal.tsx
│   │       │   ├── ImportModal.tsx
│   │       │   ├── LocalPickModal.tsx
│   │       │   ├── NewToolsModal.tsx
│   │       │   ├── ScopeSyncModal.tsx
│   │       │   └── SharedDirModal.tsx
│   │       └── types.ts
│   ├── i18n/
│   │   ├── index.ts
│   │   └── resources.ts
│   ├── index.css
│   ├── main.tsx
│   ├── pages/
│   │   └── Dashboard.tsx
│   └── tauri-plugin-dialog.d.ts
├── src-tauri/
│   ├── .gitignore
│   ├── Cargo.toml
│   ├── build.rs
│   ├── capabilities/
│   │   └── default.json
│   ├── icons/
│   │   └── icon.icns
│   ├── src/
│   │   ├── commands/
│   │   │   ├── mod.rs
│   │   │   └── tests/
│   │   │       └── commands.rs
│   │   ├── core/
│   │   │   ├── cache_cleanup.rs
│   │   │   ├── cancel_token.rs
│   │   │   ├── central_repo.rs
│   │   │   ├── content_hash.rs
│   │   │   ├── featured_skills.rs
│   │   │   ├── git_fetcher.rs
│   │   │   ├── github_download.rs
│   │   │   ├── github_search.rs
│   │   │   ├── installer.rs
│   │   │   ├── mod.rs
│   │   │   ├── onboarding.rs
│   │   │   ├── skill_files.rs
│   │   │   ├── skill_store.rs
│   │   │   ├── skills_search.rs
│   │   │   ├── sync_engine.rs
│   │   │   ├── temp_cleanup.rs
│   │   │   ├── tests/
│   │   │   │   ├── central_repo.rs
│   │   │   │   ├── content_hash.rs
│   │   │   │   ├── featured_skills.rs
│   │   │   │   ├── git_fetcher.rs
│   │   │   │   ├── github_search.rs
│   │   │   │   ├── installer.rs
│   │   │   │   ├── onboarding.rs
│   │   │   │   ├── skill_store.rs
│   │   │   │   ├── skills_search.rs
│   │   │   │   ├── sync_engine.rs
│   │   │   │   ├── temp_cleanup.rs
│   │   │   │   └── tool_adapters.rs
│   │   │   └── tool_adapters/
│   │   │       └── mod.rs
│   │   ├── lib.rs
│   │   └── main.rs
│   └── tauri.conf.json
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
Download .txt
SYMBOL INDEX (453 symbols across 57 files)

FILE: scripts/extract-changelog.mjs
  function normalizeVersion (line 3) | function normalizeVersion(input) {
  function extractSection (line 9) | function extractSection(changelogText, version) {
  function listVersions (line 38) | function listVersions(changelogText) {
  function escapeRegExp (line 51) | function escapeRegExp(str) {

FILE: scripts/fetch-featured-skills.mjs
  constant OUTPUT_FILE (line 31) | const OUTPUT_FILE = 'featured-skills.json'
  constant CURATED_REPOS (line 34) | const CURATED_REPOS = [
  constant MAX_SKILLS (line 44) | const MAX_SKILLS = 300
  constant CONCURRENCY (line 45) | const CONCURRENCY = 10
  constant MAX_RATE_LIMIT_WAIT_SECS (line 46) | const MAX_RATE_LIMIT_WAIT_SECS = 60
  constant SKILL_SCAN_BASES (line 49) | const SKILL_SCAN_BASES = [
  constant ROOT_SKIP_DIRS (line 58) | const ROOT_SKIP_DIRS = new Set([
  constant CATEGORY_RULES (line 65) | const CATEGORY_RULES = [
  constant GITHUB_TOKEN (line 75) | const GITHUB_TOKEN = process.env.GITHUB_TOKEN || ''
  function fetchJson (line 79) | async function fetchJson(url, retries = 3) {
  function sleep (line 119) | function sleep(ms) {
  function pMap (line 124) | async function pMap(items, fn, concurrency) {
  function fetchRepoMetadata (line 142) | async function fetchRepoMetadata(fullName) {
  function fetchAllRepoMetadata (line 152) | async function fetchAllRepoMetadata() {
  function detectSkillsFromTree (line 169) | function detectSkillsFromTree(treeItems) {
  function getRepoTree (line 219) | async function getRepoTree(owner, repo, branch) {
  function classify (line 231) | function classify(topics, description) {
  function parseSkillMdFrontmatter (line 243) | function parseSkillMdFrontmatter(content) {
  function fetchSkillMdContent (line 260) | async function fetchSkillMdContent(owner, repo, branch, dirPath) {
  function kebabToTitle (line 271) | function kebabToTitle(name) {
  function slugFromDirPath (line 278) | function slugFromDirPath(dirPath, repoName) {
  function main (line 286) | async function main() {

FILE: scripts/version.mjs
  constant ROOT (line 6) | const ROOT = process.cwd();
  function read (line 8) | function read(filePath) {
  function write (line 12) | function write(filePath, contents) {
  function replaceJsonStringProp (line 16) | function replaceJsonStringProp(filePath, propName, newValue) {
  function replaceCargoPackageVersion (line 27) | function replaceCargoPackageVersion(filePath, newValue) {
  function getPackageJsonVersion (line 50) | function getPackageJsonVersion() {
  function setPackageJsonVersion (line 58) | function setPackageJsonVersion(newVersion) {
  function syncFromPackageJson (line 62) | function syncFromPackageJson() {
  function checkInSync (line 71) | function checkInSync() {
  function usage (line 98) | function usage() {
  function main (line 105) | async function main() {

FILE: src-tauri/build.rs
  function main (line 1) | fn main() {

FILE: src-tauri/src/commands/mod.rs
  constant RECENT_PROJECTS_SETTING (line 37) | const RECENT_PROJECTS_SETTING: &str = "recent_projects_v1";
  function format_anyhow_error (line 39) | fn format_anyhow_error(err: anyhow::Error) -> String {
  type ToolInfoDto (line 109) | pub struct ToolInfoDto {
  type ToolStatusDto (line 118) | pub struct ToolStatusDto {
  function get_tool_status (line 125) | pub async fn get_tool_status(store: State<'_, SkillStore>) -> Result<Too...
  function get_onboarding_plan (line 180) | pub async fn get_onboarding_plan(
  function get_git_cache_cleanup_days (line 192) | pub async fn get_git_cache_cleanup_days(store: State<'_, SkillStore>) ->...
  function set_git_cache_cleanup_days (line 203) | pub async fn set_git_cache_cleanup_days(
  function clear_git_cache_now (line 215) | pub async fn clear_git_cache_now(app: tauri::AppHandle) -> Result<usize,...
  function get_git_cache_ttl_secs (line 225) | pub async fn get_git_cache_ttl_secs(store: State<'_, SkillStore>) -> Res...
  function set_git_cache_ttl_secs (line 236) | pub async fn set_git_cache_ttl_secs(
  type InstallResultDto (line 248) | pub struct InstallResultDto {
  function expand_home_path (line 255) | fn expand_home_path(input: &str) -> Result<std::path::PathBuf, anyhow::E...
  function normalize_scope (line 271) | fn normalize_scope(scope: Option<&str>) -> Result<&'static str, anyhow::...
  function get_recent_projects (line 280) | pub async fn get_recent_projects(store: State<'_, SkillStore>) -> Result...
  function save_recent_project (line 290) | pub async fn save_recent_project(
  function get_recent_projects_impl (line 301) | fn get_recent_projects_impl(store: &SkillStore) -> Result<Vec<String>, a...
  function save_recent_project_impl (line 309) | fn save_recent_project_impl(
  function get_central_repo_path (line 330) | pub async fn get_central_repo_path(
  function set_central_repo_path (line 346) | pub async fn set_central_repo_path(
  function install_local (line 406) | pub async fn install_local(
  function list_local_skills_cmd (line 424) | pub async fn list_local_skills_cmd(basePath: String) -> Result<Vec<Local...
  function install_local_selection (line 436) | pub async fn install_local_selection(
  function install_git (line 457) | pub async fn install_git(
  function list_git_skills_cmd (line 478) | pub async fn list_git_skills_cmd(
  function install_git_selection (line 492) | pub async fn install_git_selection(
  type SyncResultDto (line 510) | pub struct SyncResultDto {
  function sync_skill_dir (line 516) | pub async fn sync_skill_dir(
  function sync_skill_to_tool (line 541) | pub async fn sync_skill_to_tool(
  function target_has_same_content (line 675) | fn target_has_same_content(source: &std::path::Path, target: &std::path:...
  function unsync_skill_from_tool (line 687) | pub async fn unsync_skill_from_tool(
  type UpdateResultDto (line 756) | pub struct UpdateResultDto {
  function update_managed_skill (line 766) | pub async fn update_managed_skill(
  function search_github (line 788) | pub async fn search_github(
  function get_github_token (line 810) | pub async fn get_github_token(store: State<'_, SkillStore>) -> Result<St...
  function set_github_token (line 821) | pub async fn set_github_token(store: State<'_, SkillStore>, token: Strin...
  function import_existing_skill (line 839) | pub async fn import_existing_skill(
  type ManagedSkillDto (line 862) | pub struct ManagedSkillDto {
  type TagDto (line 878) | pub struct TagDto {
  type TagWithCountDto (line 884) | pub struct TagWithCountDto {
  type SkillTargetDto (line 892) | pub struct SkillTargetDto {
  function get_managed_skills (line 903) | pub fn get_managed_skills(store: State<'_, SkillStore>) -> Result<Vec<Ma...
  function get_tags (line 908) | pub fn get_tags(store: State<'_, SkillStore>) -> Result<Vec<TagWithCount...
  function create_tag (line 926) | pub fn create_tag(store: State<'_, SkillStore>, name: String) -> Result<...
  function rename_tag (line 938) | pub fn rename_tag(
  function delete_tag (line 954) | pub fn delete_tag(store: State<'_, SkillStore>, tagId: i64) -> Result<()...
  function get_skill_tags (line 960) | pub fn get_skill_tags(
  function set_skill_tags (line 979) | pub fn set_skill_tags(
  function get_untagged_skill_ids (line 990) | pub fn get_untagged_skill_ids(store: State<'_, SkillStore>) -> Result<Ve...
  function delete_managed_skill (line 996) | pub async fn delete_managed_skill(
  function remove_path_any (line 1039) | fn remove_path_any(path: &str) -> Result<(), String> {
  function to_install_dto (line 1063) | fn to_install_dto(result: InstallResult) -> InstallResultDto {
  function now_ms (line 1072) | fn now_ms() -> i64 {
  function get_managed_skills_impl (line 1079) | fn get_managed_skills_impl(store: &SkillStore) -> Result<Vec<ManagedSkil...
  type FeaturedSkillDto (line 1127) | pub struct FeaturedSkillDto {
    method from (line 1137) | fn from(s: FeaturedSkill) -> Self {
  function get_featured_skills (line 1150) | pub async fn get_featured_skills(
  type OnlineSkillDto (line 1164) | pub struct OnlineSkillDto {
    method from (line 1172) | fn from(r: OnlineSkillResult) -> Self {
  function search_skills_online (line 1183) | pub async fn search_skills_online(
  type SkillFileEntry (line 1199) | pub struct SkillFileEntry {
  function list_skill_files (line 1205) | pub async fn list_skill_files(central_path: String) -> Result<Vec<SkillF...
  function read_skill_file (line 1225) | pub async fn read_skill_file(central_path: String, file_path: String) ->...
  function cancel_current_operation (line 1236) | pub fn cancel_current_operation(cancel: State<'_, Arc<CancelToken>>) -> ...

FILE: src-tauri/src/commands/tests/commands.rs
  function make_store (line 4) | fn make_store() -> (tempfile::TempDir, SkillStore) {
  function format_anyhow_error_passthrough_prefixes (line 12) | fn format_anyhow_error_passthrough_prefixes() {
  function format_anyhow_error_redacts_clone_temp_path (line 18) | fn format_anyhow_error_redacts_clone_temp_path() {
  function format_anyhow_error_github_hint_auth (line 26) | fn format_anyhow_error_github_hint_auth() {
  function expand_home_path_basic (line 33) | fn expand_home_path_basic() {
  function expand_home_path_empty_is_error (line 40) | fn expand_home_path_empty_is_error() {
  function normalize_scope_defaults_to_global_and_rejects_unknown (line 46) | fn normalize_scope_defaults_to_global_and_rejects_unknown() {
  function recent_projects_are_deduped_ordered_and_limited (line 54) | fn recent_projects_are_deduped_ordered_and_limited() {
  function save_recent_project_rejects_missing_directory (line 88) | fn save_recent_project_rejects_missing_directory() {
  function remove_path_any_handles_file_dir_and_missing (line 98) | fn remove_path_any_handles_file_dir_and_missing() {
  function remove_path_any_removes_symlink_only (line 115) | fn remove_path_any_removes_symlink_only() {
  function get_managed_skills_impl_maps_targets (line 130) | fn get_managed_skills_impl_maps_targets() {

FILE: src-tauri/src/core/cache_cleanup.rs
  constant CACHE_DIR_NAME (line 10) | const CACHE_DIR_NAME: &str = "skills-hub-git-cache";
  constant CACHE_META_FILE (line 11) | const CACHE_META_FILE: &str = ".skills-hub-cache.json";
  constant GIT_CACHE_CLEANUP_DAYS_KEY (line 12) | pub const GIT_CACHE_CLEANUP_DAYS_KEY: &str = "git_cache_cleanup_days";
  constant DEFAULT_GIT_CACHE_CLEANUP_DAYS (line 13) | pub const DEFAULT_GIT_CACHE_CLEANUP_DAYS: i64 = 30;
  constant MAX_GIT_CACHE_CLEANUP_DAYS (line 14) | const MAX_GIT_CACHE_CLEANUP_DAYS: i64 = 3650;
  constant GIT_CACHE_TTL_SECS_KEY (line 15) | pub const GIT_CACHE_TTL_SECS_KEY: &str = "git_cache_ttl_secs";
  constant DEFAULT_GIT_CACHE_TTL_SECS (line 16) | pub const DEFAULT_GIT_CACHE_TTL_SECS: i64 = 60;
  constant MAX_GIT_CACHE_TTL_SECS (line 17) | const MAX_GIT_CACHE_TTL_SECS: i64 = 3600;
  type RepoCacheMeta (line 20) | struct RepoCacheMeta {
  function get_git_cache_cleanup_days (line 24) | pub fn get_git_cache_cleanup_days(store: &SkillStore) -> i64 {
  function set_git_cache_cleanup_days (line 29) | pub fn set_git_cache_cleanup_days(store: &SkillStore, days: i64) -> Resu...
  function get_git_cache_ttl_secs (line 40) | pub fn get_git_cache_ttl_secs(store: &SkillStore) -> i64 {
  function set_git_cache_ttl_secs (line 45) | pub fn set_git_cache_ttl_secs(store: &SkillStore, secs: i64) -> Result<i...
  function cleanup_git_cache_dirs (line 56) | pub fn cleanup_git_cache_dirs<R: tauri::Runtime>(
  function cleanup_git_cache_dirs_in (line 67) | fn cleanup_git_cache_dirs_in(cache_dir: &Path, max_age: Duration) -> Res...
  function parse_cleanup_days (line 130) | fn parse_cleanup_days(raw: Option<String>) -> Option<i64> {
  function parse_cache_ttl_secs (line 139) | fn parse_cache_ttl_secs(raw: Option<String>) -> Option<i64> {
  function now_ms (line 148) | fn now_ms() -> i64 {

FILE: src-tauri/src/core/cancel_token.rs
  type CancelToken (line 6) | pub struct CancelToken {
    method new (line 11) | pub fn new() -> Self {
    method cancel (line 18) | pub fn cancel(&self) {
    method reset (line 23) | pub fn reset(&self) {
    method is_cancelled (line 28) | pub fn is_cancelled(&self) -> bool {
  function default_is_not_cancelled (line 38) | fn default_is_not_cancelled() {
  function cancel_sets_flag (line 44) | fn cancel_sets_flag() {
  function reset_clears_flag (line 51) | fn reset_clears_flag() {

FILE: src-tauri/src/core/central_repo.rs
  constant CENTRAL_DIR_NAME (line 9) | const CENTRAL_DIR_NAME: &str = ".skillshub";
  function resolve_central_repo_path (line 11) | pub fn resolve_central_repo_path<R: tauri::Runtime>(
  function ensure_central_repo (line 30) | pub fn ensure_central_repo(path: &Path) -> Result<()> {

FILE: src-tauri/src/core/content_hash.rs
  constant IGNORE_NAMES (line 7) | const IGNORE_NAMES: [&str; 4] = [".git", ".DS_Store", "Thumbs.db", ".git...
  function is_ignored (line 9) | fn is_ignored(entry: &DirEntry) -> bool {
  function hash_dir (line 14) | pub fn hash_dir(path: &Path) -> Result<String> {

FILE: src-tauri/src/core/featured_skills.rs
  constant FEATURED_SKILLS_URL (line 7) | const FEATURED_SKILLS_URL: &str =
  constant CACHE_KEY (line 10) | const CACHE_KEY: &str = "featured_skills_cache";
  constant BUNDLED_JSON (line 13) | const BUNDLED_JSON: &str = include_str!("../../../featured-skills.json");
  type FeaturedSkillsData (line 16) | struct FeaturedSkillsData {
  type FeaturedSkillRaw (line 21) | struct FeaturedSkillRaw {
  type FeaturedSkill (line 35) | pub struct FeaturedSkill {
  function fetch_featured_skills (line 44) | pub fn fetch_featured_skills(store: &SkillStore) -> Result<Vec<FeaturedS...
  function fetch_featured_skills_inner (line 48) | fn fetch_featured_skills_inner(url: &str, store: &SkillStore) -> Result<...
  function fetch_from_url (line 69) | fn fetch_from_url(url: &str) -> Result<String> {
  function parse_and_filter (line 88) | fn parse_and_filter(json_str: &str) -> Result<Vec<FeaturedSkill>> {

FILE: src-tauri/src/core/git_fetcher.rs
  function clone_or_pull (line 12) | pub fn clone_or_pull(
  function clone_or_pull_sparse (line 93) | pub fn clone_or_pull_sparse(
  function git_timeout (line 268) | fn git_timeout() -> Duration {
  function git_fetch_timeout (line 276) | fn git_fetch_timeout() -> Duration {
  function resolve_git_bin (line 286) | fn resolve_git_bin() -> Option<String> {
  function git_bin_works (line 324) | fn git_bin_works(bin: &str) -> bool {
  function git_cmd (line 335) | fn git_cmd() -> Command {
  function run_cmd_with_timeout (line 347) | fn run_cmd_with_timeout(
  function clone_or_pull_via_git_cli (line 387) | fn clone_or_pull_via_git_cli(
  function fetch_origin (line 530) | fn fetch_origin(repo: &Repository) -> Result<()> {

FILE: src-tauri/src/core/github_download.rs
  type GithubContent (line 13) | struct GithubContent {
  function download_github_directory (line 28) | pub fn download_github_directory(
  function download_dir_recursive (line 48) | fn download_dir_recursive(
  function check_github_response (line 135) | fn check_github_response(
  function parse_github_api_params (line 170) | pub fn parse_github_api_params(
  function parse_github_api_params_extracts_correctly (line 206) | fn parse_github_api_params_extracts_correctly() {
  function parse_github_api_params_returns_none_without_subpath (line 224) | fn parse_github_api_params_returns_none_without_subpath() {
  function parse_github_api_params_returns_none_for_root_subpath (line 231) | fn parse_github_api_params_returns_none_for_root_subpath() {
  function parse_github_api_params_returns_none_for_non_github (line 241) | fn parse_github_api_params_returns_none_for_non_github() {
  function check_github_response_passes_success (line 251) | fn check_github_response_passes_success() {
  function check_github_response_extracts_rate_limit_reset (line 264) | fn check_github_response_extracts_rate_limit_reset() {
  function check_github_response_handles_403_without_reset_header (line 296) | fn check_github_response_handles_403_without_reset_header() {
  function check_github_response_handles_other_errors (line 314) | fn check_github_response_handles_other_errors() {

FILE: src-tauri/src/core/github_search.rs
  type SearchResponse (line 6) | struct SearchResponse {
  type RepoItem (line 11) | struct RepoItem {
  type RepoSummary (line 21) | pub struct RepoSummary {
  function search_github_repos (line 30) | pub fn search_github_repos(
  function search_github_repos_inner (line 38) | fn search_github_repos_inner(

FILE: src-tauri/src/core/installer.rs
  type InstallResult (line 21) | pub struct InstallResult {
  function install_local_skill (line 28) | pub fn install_local_skill<R: tauri::Runtime>(
  function install_git_skill (line 87) | pub fn install_git_skill<R: tauri::Runtime>(
  type ParsedGitSource (line 305) | struct ParsedGitSource {
  function parse_github_url (line 311) | fn parse_github_url(input: &str) -> ParsedGitSource {
  function normalize_github_skill_subpath (line 381) | fn normalize_github_skill_subpath(subpath: &str) -> String {
  function looks_like_github_shorthand (line 393) | fn looks_like_github_shorthand(input: &str) -> bool {
  function now_ms (line 438) | fn now_ms() -> i64 {
  function derive_name_from_repo_url (line 445) | fn derive_name_from_repo_url(repo_url: &str) -> String {
  constant SKILL_SCAN_BASES (line 462) | const SKILL_SCAN_BASES: [&str; 5] = [
  function is_skill_dir (line 471) | fn is_skill_dir(p: &Path) -> bool {
  function ensure_installable_skill_dir (line 475) | fn ensure_installable_skill_dir(p: &Path) -> Result<()> {
  function is_claude_skill_dir (line 486) | fn is_claude_skill_dir(p: &Path) -> bool {
  function read_plugin_description (line 498) | fn read_plugin_description(repo_dir: &Path) -> Option<String> {
  function extract_skill_info (line 512) | fn extract_skill_info(skill_dir: &Path, repo_dir: &Path) -> (String, Opt...
  function is_hidden_dir_name (line 529) | fn is_hidden_dir_name(name: &str) -> bool {
  function is_known_root_scan_dir (line 533) | fn is_known_root_scan_dir(name: &str) -> bool {
  function is_skill_container_dir_name (line 540) | fn is_skill_container_dir_name(name: &str) -> bool {
  function push_skill_dirs_from_base (line 545) | fn push_skill_dirs_from_base(out: &mut Vec<PathBuf>, base_dir: &Path) {
  function collect_skill_dirs (line 556) | fn collect_skill_dirs(repo_dir: &Path) -> Vec<PathBuf> {
  function scan_skill_candidates_in_dir (line 592) | fn scan_skill_candidates_in_dir(repo_dir: &Path) -> Vec<(String, String)> {
  function count_skills_in_repo (line 607) | fn count_skills_in_repo(repo_dir: &Path) -> usize {
  function compute_content_hash (line 611) | fn compute_content_hash(path: &Path) -> Option<String> {
  function should_compute_content_hash (line 619) | fn should_compute_content_hash() -> bool {
  type UpdateResult (line 629) | pub struct UpdateResult {
  function update_managed_skill_from_source (line 639) | pub fn update_managed_skill_from_source<R: tauri::Runtime>(
  type GitSkillCandidate (line 835) | pub struct GitSkillCandidate {
  type LocalSkillCandidate (line 842) | pub struct LocalSkillCandidate {
  function list_git_skills (line 850) | pub fn list_git_skills<R: tauri::Runtime>(
  function list_local_skills (line 927) | pub fn list_local_skills(base_path: &Path) -> Result<Vec<LocalSkillCandi...
  function install_git_skill_from_selection (line 1047) | pub fn install_git_skill_from_selection<R: tauri::Runtime>(
  function install_local_skill_from_selection (line 1151) | pub fn install_local_skill_from_selection<R: tauri::Runtime>(
  type RepoCacheMeta (line 1184) | struct RepoCacheMeta {
  function clone_to_cache (line 1191) | fn clone_to_cache<R: tauri::Runtime>(
  function clone_to_cache_subpath (line 1272) | fn clone_to_cache_subpath<R: tauri::Runtime>(
  function repo_cache_key (line 1356) | fn repo_cache_key(clone_url: &str, branch: Option<&str>, subpath: Option...
  function backfill_skill_descriptions (line 1372) | pub fn backfill_skill_descriptions(store: &SkillStore) {
  function parse_skill_md (line 1388) | fn parse_skill_md(path: &Path) -> Option<(String, Option<String>)> {
  function parse_skill_md_with_reason (line 1392) | fn parse_skill_md_with_reason(path: &Path) -> Result<(String, Option<Str...
  function clean_frontmatter_value (line 1451) | fn clean_frontmatter_value(value: &str) -> String {
  function frontmatter_block_style (line 1463) | fn frontmatter_block_style(value: &str) -> Option<char> {

FILE: src-tauri/src/core/onboarding.rs
  type OnboardingVariant (line 13) | pub struct OnboardingVariant {
  type OnboardingGroup (line 23) | pub struct OnboardingGroup {
  type OnboardingPlan (line 30) | pub struct OnboardingPlan {
  function build_onboarding_plan (line 36) | pub fn build_onboarding_plan<R: tauri::Runtime>(
  function build_onboarding_plan_in_home (line 52) | fn build_onboarding_plan_in_home(
  function filter_detected (line 115) | fn filter_detected(
  function is_under (line 146) | fn is_under(path: &Path, base: &Path) -> bool {
  function managed_target_key (line 150) | fn managed_target_key(tool: &str, path: &Path) -> String {
  function normalize_path_for_key (line 156) | fn normalize_path_for_key(path: &Path) -> String {

FILE: src-tauri/src/core/skill_files.rs
  constant IGNORE_NAMES (line 6) | const IGNORE_NAMES: [&str; 4] = [".git", ".DS_Store", "Thumbs.db", ".git...
  constant MAX_FILE_SIZE (line 7) | const MAX_FILE_SIZE: u64 = 1_048_576;
  function is_ignored (line 9) | fn is_ignored(entry: &DirEntry) -> bool {
  type FileEntry (line 14) | pub struct FileEntry {
  function list_files (line 19) | pub fn list_files(central_path: &Path) -> Result<Vec<FileEntry>> {
  function read_file (line 58) | pub fn read_file(central_path: &Path, relative_path: &str) -> Result<Str...

FILE: src-tauri/src/core/skill_store.rs
  constant DB_FILE_NAME (line 7) | const DB_FILE_NAME: &str = "skills_hub.db";
  constant LEGACY_APP_IDENTIFIERS (line 8) | const LEGACY_APP_IDENTIFIERS: &[&str] = &["com.tauri.dev", "com.tauri.de...
  constant SCHEMA_VERSION (line 11) | const SCHEMA_VERSION: i32 = 5;
  constant SCHEMA_V1 (line 14) | const SCHEMA_V1: &str = r#"
  type SkillStore (line 84) | pub struct SkillStore {
    method new (line 135) | pub fn new(db_path: PathBuf) -> Self {
    method db_path (line 140) | pub fn db_path(&self) -> &Path {
    method ensure_schema (line 144) | pub fn ensure_schema(&self) -> Result<()> {
    method get_setting (line 184) | pub fn get_setting(&self, key: &str) -> Result<Option<String>> {
    method set_setting (line 195) | pub fn set_setting(&self, key: &str, value: &str) -> Result<()> {
    method set_onboarding_completed (line 207) | pub fn set_onboarding_completed(&self, completed: bool) -> Result<()> {
    method upsert_skill (line 214) | pub fn upsert_skill(&self, record: &SkillRecord) -> Result<()> {
    method upsert_skill_target (line 259) | pub fn upsert_skill_target(&self, record: &SkillTargetRecord) -> Resul...
    method list_skills (line 290) | pub fn list_skills(&self) -> Result<Vec<SkillRecord>> {
    method get_skill_by_id (line 325) | pub fn get_skill_by_id(&self, skill_id: &str) -> Result<Option<SkillRe...
    method update_skill_description (line 358) | pub fn update_skill_description(
    method delete_skill (line 372) | pub fn delete_skill(&self, skill_id: &str) -> Result<()> {
    method create_tag (line 379) | pub fn create_tag(&self, name: &str) -> Result<TagRecord> {
    method rename_tag (line 396) | pub fn rename_tag(&self, tag_id: i64, name: &str) -> Result<TagRecord> {
    method delete_tag (line 415) | pub fn delete_tag(&self, tag_id: i64) -> Result<()> {
    method list_tags_with_counts (line 422) | pub fn list_tags_with_counts(&self) -> Result<Vec<TagWithCountRecord>> {
    method get_skill_tags (line 449) | pub fn get_skill_tags(&self, skill_id: &str) -> Result<Vec<TagRecord>> {
    method set_skill_tags (line 473) | pub fn set_skill_tags(&self, skill_id: &str, tag_ids: &[i64]) -> Resul...
    method list_untagged_skill_ids (line 505) | pub fn list_untagged_skill_ids(&self) -> Result<Vec<String>> {
    method list_skill_targets (line 524) | pub fn list_skill_targets(&self, skill_id: &str) -> Result<Vec<SkillTa...
    method list_all_skill_target_paths (line 555) | pub fn list_all_skill_target_paths(&self) -> Result<Vec<(String, Strin...
    method get_skill_target (line 571) | pub fn get_skill_target(
    method delete_skill_target (line 607) | pub fn delete_skill_target(
    method with_conn (line 627) | fn with_conn<T>(&self, f: impl FnOnce(&Connection) -> Result<T>) -> Re...
  type SkillRecord (line 89) | pub struct SkillRecord {
  type SkillTargetRecord (line 107) | pub struct SkillTargetRecord {
  type TagRecord (line 121) | pub struct TagRecord {
  type TagWithCountRecord (line 127) | pub struct TagWithCountRecord {
  function migrate_skill_targets_to_v4 (line 636) | fn migrate_skill_targets_to_v4(conn: &Connection) -> Result<()> {
  function migrate_tags_to_v5 (line 667) | fn migrate_tags_to_v5(conn: &Connection) -> Result<()> {
  function normalize_tag_name (line 688) | fn normalize_tag_name(name: &str) -> Result<String> {
  function now_ms (line 696) | fn now_ms() -> i64 {
  function default_db_path (line 703) | pub fn default_db_path<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> ...
  function migrate_legacy_db_if_needed (line 713) | pub fn migrate_legacy_db_if_needed(target_db_path: &Path) -> Result<()> {
  function db_has_any_skills (line 766) | fn db_has_any_skills(db_path: &Path) -> Result<bool> {

FILE: src-tauri/src/core/skills_search.rs
  type SkillsShResponse (line 6) | struct SkillsShResponse {
  type SkillsShItem (line 11) | struct SkillsShItem {
  type OnlineSkillResult (line 18) | pub struct OnlineSkillResult {
  function search_skills_online (line 25) | pub fn search_skills_online(query: &str, limit: usize) -> Result<Vec<Onl...
  function search_skills_online_inner (line 29) | fn search_skills_online_inner(

FILE: src-tauri/src/core/sync_engine.rs
  type SyncMode (line 7) | pub enum SyncMode {
  type SyncOutcome (line 15) | pub struct SyncOutcome {
  function sync_dir_hybrid (line 21) | pub fn sync_dir_hybrid(source: &Path, target: &Path) -> Result<SyncOutco...
  function sync_dir_hybrid_with_overwrite (line 60) | pub fn sync_dir_hybrid_with_overwrite(
  function sync_dir_copy_with_overwrite (line 91) | pub fn sync_dir_copy_with_overwrite(
  function sync_dir_for_tool_with_overwrite (line 117) | pub fn sync_dir_for_tool_with_overwrite(
  function ensure_parent_dir (line 130) | fn ensure_parent_dir(path: &Path) -> Result<()> {
  function remove_path_any (line 137) | fn remove_path_any(path: &Path) -> Result<()> {
  function is_same_link (line 158) | fn is_same_link(link_path: &Path, target: &Path) -> bool {
  function try_link_dir (line 165) | fn try_link_dir(source: &Path, target: &Path) -> Result<()> {
  function try_junction (line 185) | fn try_junction(source: &Path, target: &Path) -> Result<()> {
  function should_skip_copy (line 191) | fn should_skip_copy(entry: &walkdir::DirEntry) -> bool {
  function copy_dir_recursive (line 195) | pub fn copy_dir_recursive(source: &Path, target: &Path) -> Result<()> {

FILE: src-tauri/src/core/temp_cleanup.rs
  constant TEMP_PREFIX (line 7) | const TEMP_PREFIX: &str = "skills-hub-git-";
  constant TEMP_MARKER (line 8) | const TEMP_MARKER: &str = ".skills-hub-git-temp";
  function mark_temp_dir (line 11) | pub fn mark_temp_dir(dir: &Path) -> Result<()> {
  function cleanup_old_git_temp_dirs (line 21) | pub fn cleanup_old_git_temp_dirs<R: tauri::Runtime>(
  function cleanup_old_git_temp_dirs_in (line 33) | fn cleanup_old_git_temp_dirs_in(cache_dir: &Path, max_age: Duration) -> ...

FILE: src-tauri/src/core/tests/central_repo.rs
  function make_store (line 6) | fn make_store() -> (tempfile::TempDir, SkillStore) {
  function resolve_uses_setting_when_present (line 14) | fn resolve_uses_setting_when_present() {
  function ensure_central_repo_creates_dir (line 27) | fn ensure_central_repo_creates_dir() {

FILE: src-tauri/src/core/tests/content_hash.rs
  function hash_changes_with_content_and_ignores_git_dir (line 6) | fn hash_changes_with_content_and_ignores_git_dir() {

FILE: src-tauri/src/core/tests/featured_skills.rs
  function temp_store (line 4) | fn temp_store() -> SkillStore {
  function json_payload (line 14) | fn json_payload() -> String {
  function parses_and_filters_empty_source_url (line 40) | fn parses_and_filters_empty_source_url() {
  function falls_back_to_cache_on_http_failure (line 59) | fn falls_back_to_cache_on_http_failure() {
  function falls_back_to_bundled_on_total_failure (line 81) | fn falls_back_to_bundled_on_total_failure() {
  function falls_back_to_bundled_on_malformed_json (line 99) | fn falls_back_to_bundled_on_malformed_json() {

FILE: src-tauri/src/core/tests/git_fetcher.rs
  function commit_file (line 5) | fn commit_file(repo: &git2::Repository, path: &str, content: &[u8], msg:...
  function clone_then_pull_updates_head (line 29) | fn clone_then_pull_updates_head() {
  function sparse_clone_only_materializes_requested_subpath (line 59) | fn sparse_clone_only_materializes_requested_subpath() {

FILE: src-tauri/src/core/tests/github_search.rs
  function json_one_repo (line 5) | fn json_one_repo() -> String {
  function limit_is_clamped (line 22) | fn limit_is_clamped() {
  function maps_fields (line 54) | fn maps_fields() {
  function http_error_has_context (line 73) | fn http_error_has_context() {

FILE: src-tauri/src/core/tests/installer.rs
  function make_store (line 6) | fn make_store() -> (tempfile::TempDir, SkillStore) {
  function set_central_path (line 13) | fn set_central_path(store: &SkillStore, central: &Path) {
  function init_git_repo (line 19) | fn init_git_repo(dir: &Path) -> git2::Repository {
  function commit_all (line 36) | fn commit_all(repo: &git2::Repository, msg: &str) -> git2::Oid {
  function parses_github_urls (line 61) | fn parses_github_urls() {
  function parses_skill_md_frontmatter (line 102) | fn parses_skill_md_frontmatter() {
  function parses_skill_md_frontmatter_literal_description (line 123) | fn parses_skill_md_frontmatter_literal_description() {
  function parses_skill_md_frontmatter_folded_chomp_description (line 150) | fn parses_skill_md_frontmatter_folded_chomp_description() {
  function backfill_skill_descriptions_replaces_stale_frontmatter_marker (line 179) | fn backfill_skill_descriptions_replaces_stale_frontmatter_marker() {
  function installs_local_skill_and_updates_from_source (line 222) | fn installs_local_skill_and_updates_from_source() {
  function lists_and_installs_git_skills_without_network (line 293) | fn lists_and_installs_git_skills_without_network() {
  function install_git_skill_errors_on_multi_skills_repo_root (line 332) | fn install_git_skill_errors_on_multi_skills_repo_root() {
  function lists_local_skills_with_invalid_entries (line 368) | fn lists_local_skills_with_invalid_entries() {
  function install_local_selection_validates_skill_md (line 402) | fn install_local_selection_validates_skill_md() {
  function install_git_skill_uses_skill_md_name_over_subpath_skills (line 446) | fn install_git_skill_uses_skill_md_name_over_subpath_skills() {
  function install_git_skill_rejects_container_subpath_without_skill_md (line 487) | fn install_git_skill_rejects_container_subpath_without_skill_md() {
  function install_git_skill_selection_accepts_specific_child_under_container (line 524) | fn install_git_skill_selection_accepts_specific_child_under_container() {
  function install_git_skill_respects_user_provided_name (line 562) | fn install_git_skill_respects_user_provided_name() {
  function install_git_skill_derives_name_from_skill_md (line 590) | fn install_git_skill_derives_name_from_skill_md() {
  function install_git_skill_detects_root_level_multi_skills (line 623) | fn install_git_skill_detects_root_level_multi_skills() {
  function list_git_skills_finds_root_level_skills (line 662) | fn list_git_skills_finds_root_level_skills() {
  function list_git_skills_finds_root_skill_container_layout (line 704) | fn list_git_skills_finds_root_skill_container_layout() {
  function collect_skill_dirs_finds_skills_under_explicit_container (line 738) | fn collect_skill_dirs_finds_skills_under_explicit_container() {
  function collect_skill_dirs_finds_multiple_skills_under_explicit_container (line 762) | fn collect_skill_dirs_finds_multiple_skills_under_explicit_container() {
  function collect_skill_dirs_scans_named_skill_containers_but_not_generic_dirs (line 795) | fn collect_skill_dirs_scans_named_skill_containers_but_not_generic_dirs() {
  function collect_skill_dirs_deduplicates_known_root_containers (line 824) | fn collect_skill_dirs_deduplicates_known_root_containers() {

FILE: src-tauri/src/core/tests/onboarding.rs
  function groups_by_name_and_detects_conflicts_by_fingerprint (line 6) | fn groups_by_name_and_detects_conflicts_by_fingerprint() {
  function excludes_central_repo_path (line 34) | fn excludes_central_repo_path() {
  function excludes_managed_skill_targets (line 54) | fn excludes_managed_skill_targets() {

FILE: src-tauri/src/core/tests/skill_store.rs
  function make_store (line 6) | fn make_store() -> (tempfile::TempDir, SkillStore) {
  function make_skill (line 14) | fn make_skill(id: &str, name: &str, central_path: &str, updated_at: i64)...
  function schema_is_idempotent (line 34) | fn schema_is_idempotent() {
  function migrates_v3_targets_to_global_scope (line 40) | fn migrates_v3_targets_to_global_scope() {
  function settings_roundtrip_and_update (line 114) | fn settings_roundtrip_and_update() {
  function skills_upsert_list_get_delete (line 142) | fn skills_upsert_list_get_delete() {
  function skill_targets_upsert_unique_constraint_and_list_order (line 170) | fn skill_targets_upsert_unique_constraint_and_list_order() {
  function project_targets_coexist_by_project_path_and_delete_precisely (line 248) | fn project_targets_coexist_by_project_path_and_delete_precisely() {
  function deleting_skill_cascades_targets (line 355) | fn deleting_skill_cascades_targets() {
  function tags_can_be_created_renamed_linked_and_deleted (line 380) | fn tags_can_be_created_renamed_linked_and_deleted() {
  function tag_links_are_removed_when_skill_is_deleted_and_untagged_is_counted (line 418) | fn tag_links_are_removed_when_skill_is_deleted_and_untagged_is_counted() {
  function description_stored_and_retrieved (line 436) | fn description_stored_and_retrieved() {
  function description_null_by_default (line 447) | fn description_null_by_default() {
  function update_skill_description_backfills (line 457) | fn update_skill_description_backfills() {
  function error_context_includes_db_path (line 484) | fn error_context_includes_db_path() {

FILE: src-tauri/src/core/tests/skills_search.rs
  function json_response (line 5) | fn json_response() -> String {
  function json_empty (line 24) | fn json_empty() -> String {
  function parses_search_results (line 29) | fn parses_search_results() {
  function source_url_is_constructed_from_source (line 54) | fn source_url_is_constructed_from_source() {
  function http_error_returns_error (line 72) | fn http_error_returns_error() {
  function empty_results (line 86) | fn empty_results() {

FILE: src-tauri/src/core/tests/sync_engine.rs
  function copy_dir_recursive_skips_git_dir (line 9) | fn copy_dir_recursive_skips_git_dir() {
  function hybrid_sync_creates_link_and_is_idempotent_when_same_link (line 24) | fn hybrid_sync_creates_link_and_is_idempotent_when_same_link() {
  function hybrid_sync_with_overwrite_replaces_existing (line 46) | fn hybrid_sync_with_overwrite_replaces_existing() {
  function cursor_sync_forces_copy (line 63) | fn cursor_sync_forces_copy() {
  function copy_overwrite_replaces_broken_symlink_target (line 79) | fn copy_overwrite_replaces_broken_symlink_target() {

FILE: src-tauri/src/core/tests/temp_cleanup.rs
  function cleanup_removes_only_marked_prefixed_dirs (line 7) | fn cleanup_removes_only_marked_prefixed_dirs() {

FILE: src-tauri/src/core/tests/tool_adapters.rs
  function adapter_by_key_finds_known_tool (line 10) | fn adapter_by_key_finds_known_tool() {
  function adapter_by_key_finds_new_tools (line 16) | fn adapter_by_key_finds_new_tools() {
  function adapters_sharing_skills_dir_groups_amp_and_kimi (line 26) | fn adapters_sharing_skills_dir_groups_amp_and_kimi() {
  function project_relative_skills_dir_maps_supported_agents (line 36) | fn project_relative_skills_dir_maps_supported_agents() {
  function project_path_resolution_uses_project_specific_mapping (line 73) | fn project_path_resolution_uses_project_specific_mapping() {
  function adapters_sharing_project_skills_dir_groups_agents_tools (line 94) | fn adapters_sharing_project_skills_dir_groups_agents_tools() {
  function scan_tool_dir_skips_codex_system_and_includes_symlink_dir (line 114) | fn scan_tool_dir_skips_codex_system_and_includes_symlink_dir() {
  function scan_tool_dir_skips_app_support_path (line 148) | fn scan_tool_dir_skips_app_support_path() {

FILE: src-tauri/src/core/tool_adapters/mod.rs
  type ToolId (line 6) | pub enum ToolId {
    method as_key (line 54) | pub fn as_key(&self) -> &'static str {
  type ToolAdapter (line 105) | pub struct ToolAdapter {
  type DetectedSkill (line 115) | pub struct DetectedSkill {
  function default_tool_adapters (line 123) | pub fn default_tool_adapters() -> Vec<ToolAdapter> {
  function adapters_sharing_skills_dir (line 436) | pub fn adapters_sharing_skills_dir(adapter: &ToolAdapter) -> Vec<ToolAda...
  function adapters_sharing_project_skills_dir (line 443) | pub fn adapters_sharing_project_skills_dir(adapter: &ToolAdapter) -> Vec...
  function adapter_by_key (line 451) | pub fn adapter_by_key(key: &str) -> Option<ToolAdapter> {
  function resolve_default_path (line 457) | pub fn resolve_default_path(adapter: &ToolAdapter) -> Result<PathBuf> {
  function resolve_project_path (line 462) | pub fn resolve_project_path(adapter: &ToolAdapter, project_root: &Path) ...
  function supports_project_scope (line 466) | pub fn supports_project_scope(adapter: &ToolAdapter) -> bool {
  function project_relative_skills_dir (line 470) | pub fn project_relative_skills_dir(adapter: &ToolAdapter) -> &'static str {
  function resolve_detect_path (line 517) | pub fn resolve_detect_path(adapter: &ToolAdapter) -> Result<PathBuf> {
  function is_tool_installed (line 522) | pub fn is_tool_installed(adapter: &ToolAdapter) -> Result<bool> {
  function scan_tool_dir (line 526) | pub fn scan_tool_dir(tool: &ToolAdapter, dir: &Path) -> Result<Vec<Detec...
  function detect_link (line 568) | fn detect_link(path: &Path) -> (bool, Option<PathBuf>) {

FILE: src-tauri/src/lib.rs
  function run (line 12) | pub fn run() {

FILE: src-tauri/src/main.rs
  function main (line 4) | fn main() {

FILE: src/App.tsx
  type SkillScopeState (line 39) | type SkillScopeState = Record<
  function App (line 47) | function App() {

FILE: src/components/skills/ExplorePage.tsx
  type ExplorePageProps (line 6) | type ExplorePageProps = {
  function formatCount (line 20) | function formatCount(n: number): string {

FILE: src/components/skills/FilterBar.tsx
  type FilterBarProps (line 6) | type FilterBarProps = {

FILE: src/components/skills/Header.tsx
  type HeaderProps (line 5) | type HeaderProps = {

FILE: src/components/skills/LoadingOverlay.tsx
  type LoadingOverlayProps (line 4) | type LoadingOverlayProps = {

FILE: src/components/skills/SettingsPage.tsx
  type UpdateStatus (line 6) | type UpdateStatus = 'idle' | 'checking' | 'up-to-date' | 'available' | '...
  type SettingsPageProps (line 8) | type SettingsPageProps = {

FILE: src/components/skills/SkillCard.tsx
  type GithubInfo (line 7) | type GithubInfo = {
  type SkillCardProps (line 12) | type SkillCardProps = {
  constant MAX_VISIBLE_BADGES (line 30) | const MAX_VISIBLE_BADGES = 5

FILE: src/components/skills/SkillDetailView.tsx
  type SkillDetailViewProps (line 25) | type SkillDetailViewProps = {
  type TreeNode (line 33) | type TreeNode = {
  function formatSize (line 42) | function formatSize(bytes: number): string {
  constant EXT_LANG (line 48) | const EXT_LANG: Record<string, string> = {
  function getLang (line 96) | function getLang(filename: string): string {
  function isMarkdown (line 104) | function isMarkdown(filename: string): boolean {
  function buildTree (line 109) | function buildTree(files: SkillFileEntry[]): TreeNode[] {
  type FileTreeNodeProps (line 163) | type FileTreeNodeProps = {
  type FileContentRendererProps (line 235) | type FileContentRendererProps = {
  function parseFrontmatter (line 241) | function parseFrontmatter(raw: string): {

FILE: src/components/skills/SkillsList.tsx
  type GithubInfo (line 7) | type GithubInfo = {
  type SkillsListProps (line 12) | type SkillsListProps = {

FILE: src/components/skills/TagsPage.tsx
  type TagsPageProps (line 6) | type TagsPageProps = {

FILE: src/components/skills/modals/AddSkillModal.tsx
  type AddSkillModalProps (line 6) | type AddSkillModalProps = {

FILE: src/components/skills/modals/DeleteModal.tsx
  type DeleteModalProps (line 5) | type DeleteModalProps = {

FILE: src/components/skills/modals/EditSkillTagsModal.tsx
  type EditSkillTagsModalProps (line 6) | type EditSkillTagsModalProps = {

FILE: src/components/skills/modals/GitPickModal.tsx
  type GitPickModalProps (line 6) | type GitPickModalProps = {

FILE: src/components/skills/modals/ImportModal.tsx
  type ImportModalProps (line 6) | type ImportModalProps = {

FILE: src/components/skills/modals/LocalPickModal.tsx
  type LocalPickModalProps (line 6) | type LocalPickModalProps = {

FILE: src/components/skills/modals/NewToolsModal.tsx
  type NewToolsModalProps (line 4) | type NewToolsModalProps = {

FILE: src/components/skills/modals/ScopeSyncModal.tsx
  type ScopeSyncModalProps (line 6) | type ScopeSyncModalProps = {

FILE: src/components/skills/modals/SharedDirModal.tsx
  type SharedDirModalProps (line 4) | type SharedDirModalProps = {

FILE: src/components/skills/types.ts
  type OnboardingVariant (line 1) | type OnboardingVariant = {
  type OnboardingGroup (line 10) | type OnboardingGroup = {
  type OnboardingPlan (line 16) | type OnboardingPlan = {
  type ToolOption (line 22) | type ToolOption = {
  type TagDto (line 28) | type TagDto = {
  type TagWithCountDto (line 33) | type TagWithCountDto = TagDto & {
  type ManagedSkill (line 38) | type ManagedSkill = {
  type GitSkillCandidate (line 61) | type GitSkillCandidate = {
  type LocalSkillCandidate (line 67) | type LocalSkillCandidate = {
  type InstallResultDto (line 75) | type InstallResultDto = {
  type ToolInfoDto (line 82) | type ToolInfoDto = {
  type ToolStatusDto (line 90) | type ToolStatusDto = {
  type UpdateResultDto (line 96) | type UpdateResultDto = {
  type FeaturedSkillDto (line 104) | type FeaturedSkillDto = {
  type OnlineSkillDto (line 113) | type OnlineSkillDto = {
  type SkillFileEntry (line 120) | type SkillFileEntry = {

FILE: src/tauri-plugin-dialog.d.ts
  type OpenDialogOptions (line 2) | type OpenDialogOptions = {
Condensed preview — 124 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,335K chars).
[
  {
    "path": ".github/workflows/ci.yml",
    "chars": 1099,
    "preview": "name: CI\n\non:\n  pull_request:\n  push:\n    branches: [main]\n\nconcurrency:\n  group: ci-${{ github.ref }}\n  cancel-in-progr"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 16223,
    "preview": "name: Release\n\non:\n  push:\n    tags:\n      - \"v*\"\n  workflow_dispatch:\n\npermissions:\n  contents: write\n\nconcurrency:\n  g"
  },
  {
    "path": ".github/workflows/update-featured-skills.yml",
    "chars": 772,
    "preview": "name: Update Featured Skills\n\non:\n  schedule:\n    - cron: '0 0 * * *'\n  workflow_dispatch:\n\npermissions:\n  contents: wri"
  },
  {
    "path": ".gitignore",
    "chars": 300,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
  },
  {
    "path": "AGENTS.md",
    "chars": 7458,
    "preview": "# Skills Hub - Project Rules\n\n## Overview\n\nSkills Hub is a cross-platform desktop app (Tauri 2 + React 19) for managing "
  },
  {
    "path": "CHANGELOG.md",
    "chars": 9773,
    "preview": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n## [Unreleased]\n\n## [0.6.0] - 2026-05"
  },
  {
    "path": "CLAUDE.md",
    "chars": 62,
    "preview": "Read and follow all instructions in [AGENTS.md](./AGENTS.md).\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 4770,
    "preview": "# Code of Conduct\n\nThis project follows the Contributor Covenant Code of Conduct (v2.1).\n\n## Our Pledge\n\nWe as members, "
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 995,
    "preview": "# Contributing\n\nThanks for taking the time to contribute to Skills Hub!\n\n## Development Requirements\n\n- Node.js 18+ (rec"
  },
  {
    "path": "LICENSE",
    "chars": 1080,
    "preview": "MIT License\n\nCopyright (c) 2026 Skills Hub contributors\n\nPermission is hereby granted, free of charge, to any person obt"
  },
  {
    "path": "README.md",
    "chars": 8135,
    "preview": "# Skills Hub (Tauri Desktop)\n\nA cross-platform desktop app (Tauri + React) to manage Agent Skills in one place and sync "
  },
  {
    "path": "SECURITY.md",
    "chars": 662,
    "preview": "# Security Policy\n\n## Supported Versions\n\nOnly the latest code on the `main` branch is supported.\n\n## Reporting a Vulner"
  },
  {
    "path": "docs/CHANGELOG.zh.md",
    "chars": 5411,
    "preview": "# 更新日志\n\n本文件记录项目的重要变更(中文版本)。\n\n## [Unreleased]\n\n## [0.6.0] - 2026-05-05\n\n### 新增\n- **Skill 标签**:可为已托管 Skill 添加自定义标签,方便整理和筛选"
  },
  {
    "path": "docs/README.zh.md",
    "chars": 6274,
    "preview": "# Skills Hub(Tauri Desktop)\n\n一个跨平台桌面应用(Tauri + React),用于统一管理 Agent Skills,并把它们同步到多种 AI 编程工具的全局或项目级 skills 目录(优先 symlink/"
  },
  {
    "path": "docs/future/profile-requirements.md",
    "chars": 4396,
    "preview": "# Skill Profile / 配置方案后续需求记录\n\n## 背景\n\nGitHub Issue #23 提到“技能分组”能力:\n\n- Skill 作为底层资产存在。\n- 单个 Skill 可以被划分到多个组中复用。\n- 组支持快速挂载与"
  },
  {
    "path": "docs/releases/v0.1-v0.2/system-design.md",
    "chars": 7208,
    "preview": "# Skills Hub (Tauri Desktop) — System Design\n\nThis document describes the system design of **Skills Hub**, aligned with "
  },
  {
    "path": "docs/releases/v0.1-v0.2/system-design.zh.md",
    "chars": 19761,
    "preview": "# Skills Hub(Tauri Desktop)系统设计文档\n\n> English version: [`docs/system-design.md`](system-design.md)\n\n> 基于当前仓库实现(commit `b5"
  },
  {
    "path": "docs/releases/v0.3.0/plan-bug-fixes.md",
    "chars": 6476,
    "preview": "# Bug 修复计划\n\n来源:https://github.com/qufei1993/skills-hub/issues\n\n---\n\n## Bug 1:Git 安装时 skill 名称为 \"skills\" 导致路径重复(#28)\n\n**I"
  },
  {
    "path": "docs/releases/v0.3.0/plan-explore-page-redesign.md",
    "chars": 2516,
    "preview": "# 需求:Explore 页面独立化 + My Skills 列表优化\n\n## 背景\n\n当前\"添加 Skill\"的交互流程存在问题:探索、本地添加、Git 添加三个功能挤在一个弹窗的三个 Tab 里。用户在探索 Tab 点击一个 skill"
  },
  {
    "path": "docs/releases/v0.3.0/plan-featured-skills.md",
    "chars": 6439,
    "preview": "# 需求一:精选技能推荐列表 — 实施计划\n\n## Context\n\n用户打开\"添加技能\"时,只有手动输入本地路径或 Git URL,缺乏发现能力。需要在 AddSkillModal 中新增\"探索\"标签页,展示由 CI 预生成的热门技能列表"
  },
  {
    "path": "docs/releases/v0.3.0/plan-online-search.md",
    "chars": 6848,
    "preview": "# 需求二:在线技能搜索与安装 — 实施计划(已完成)\n\n## Context\n\n需求一已实现\"探索\"标签页,展示精选技能列表。但精选列表覆盖有限,用户有明确需求时(如\"找 React 相关技能\"),需要实时搜索能力。搜索功能内嵌在探索标签"
  },
  {
    "path": "docs/releases/v0.3.0/plan-skill-detail-view.md",
    "chars": 8917,
    "preview": "# 技能详情页(Skill Detail View)— 实施计划(已完成)\n\n## Context\n\n当前已安装的技能以卡片列表展示,但无法查看技能的具体文件内容。用户希望点击技能卡片后能看到技能的所有文件,默认显示 SKILL.md,支持"
  },
  {
    "path": "docs/releases/v0.3.1/plan-in-app-update.md",
    "chars": 1013,
    "preview": "# 需求:应用内检查更新(Issue #33)\n\n## Context\n\n来源:https://github.com/qufei1993/skills-hub/issues/33\n\n用户每次跟进版本都需要手动进入 GitHub releas"
  },
  {
    "path": "docs/releases/v0.3.1/plan-qoderwork-support.md",
    "chars": 843,
    "preview": "# 需求:支持 QoderWork 目录(Issue #34)\n\n## Context\n\n来源:https://github.com/qufei1993/skills-hub/issues/34\n\nQoderWork 是 Qoder 推出的"
  },
  {
    "path": "docs/releases/v0.3.1/plan-settings-page.md",
    "chars": 1418,
    "preview": "# 需求:设置弹窗改为独立页面\n\n## Context\n\n设置功能原来是一个 560px 宽的模态弹窗(SettingsModal),在小窗口下容易被遮挡,需要最大化窗口才能完整查看。将其改为 `activeView` 视图系统中的独立页面"
  },
  {
    "path": "docs/releases/v0.3.1/skills-aggregation-repo.md",
    "chars": 6272,
    "preview": "# 需求:Skills 聚合数据源升级 — 精选仓库列表方案\n\n## 背景\n\nSkills Hub 应用的\"探索\"功能依赖 `featured-skills.json` 提供精选技能列表。早期方案通过 GitHub Search API 搜"
  },
  {
    "path": "docs/releases/v0.4.0/bugfix-language-toggle-loading-overlay.md",
    "chars": 965,
    "preview": "# Bugfix:切换语言时闪现 \"Installing Skills...\" 弹窗\n\n## 问题描述\n\n在 Explore 页面点击语言切换按钮(EN/中)时,会短暂弹出 \"Installing Skills...\" 加载遮罩,一两秒后自"
  },
  {
    "path": "docs/releases/v0.4.0/plan-in-app-update.md",
    "chars": 1013,
    "preview": "# 需求:应用内检查更新(Issue #33)\n\n## Context\n\n来源:https://github.com/qufei1993/skills-hub/issues/33\n\n用户每次跟进版本都需要手动进入 GitHub releas"
  },
  {
    "path": "docs/releases/v0.4.0/plan-qoderwork-support.md",
    "chars": 843,
    "preview": "# 需求:支持 QoderWork 目录(Issue #34)\n\n## Context\n\n来源:https://github.com/qufei1993/skills-hub/issues/34\n\nQoderWork 是 Qoder 推出的"
  },
  {
    "path": "docs/releases/v0.4.0/plan-settings-page.md",
    "chars": 1418,
    "preview": "# 需求:设置弹窗改为独立页面\n\n## Context\n\n设置功能原来是一个 560px 宽的模态弹窗(SettingsModal),在小窗口下容易被遮挡,需要最大化窗口才能完整查看。将其改为 `activeView` 视图系统中的独立页面"
  },
  {
    "path": "docs/releases/v0.4.0/skills-aggregation-repo.md",
    "chars": 6272,
    "preview": "# 需求:Skills 聚合数据源升级 — 精选仓库列表方案\n\n## 背景\n\nSkills Hub 应用的\"探索\"功能依赖 `featured-skills.json` 提供精选技能列表。早期方案通过 GitHub Search API 搜"
  },
  {
    "path": "docs/releases/v0.4.1/plan-frontmatter-table.md",
    "chars": 1130,
    "preview": "# Plan: Skill 详情页 Frontmatter 元数据表格展示\n\n## Context\n当前 `SkillDetailView` 使用 `remarkFrontmatter` 插件,该插件只是让 react-markdown 忽"
  },
  {
    "path": "docs/releases/v0.4.2/bugfix-new-tools-modal-style.md",
    "chars": 1179,
    "preview": "# Bugfix:首次安装后打开时\"检测到新工具\"弹窗样式异常\n\n## 问题描述\n\n安装后第一次打开 Skills Hub,\"New tools detected\"(检测到新工具)弹窗的样式与其他弹窗不一致:标题文字紧贴弹窗边框顶部,操作按"
  },
  {
    "path": "docs/releases/v0.4.2/bugfix-root-skill-install-false-exists.md",
    "chars": 1484,
    "preview": "# Bugfix:从探索页安装根目录级 Skill 时误报\"已存在于 Hub\"\n\n## 问题描述\n\n在探索页搜索并点击安装某个 Skill(如 `titanwings/colleague-skill`)时,弹出错误提示:\n\n> 「.」已存在"
  },
  {
    "path": "docs/releases/v0.4.3/add-copaw-support.md",
    "chars": 682,
    "preview": "# 新增:支持 Copaw 工具\n\n> 贡献者:[@LeonDevLifeLog](https://github.com/LeonDevLifeLog),PR [qufei1993/skills-hub#50](https://github"
  },
  {
    "path": "docs/releases/v0.4.3/bugfix-github-install-and-frontmatter.md",
    "chars": 4530,
    "preview": "# Bugfix:优化 GitHub Skill 安装速度并修复多行 Frontmatter 渲染\n\n## 问题 1:从 GitHub 仓库安装 Skill 很慢并最终超时\n\n### 问题描述\n\n从 GitHub 仓库安装某些 Skill "
  },
  {
    "path": "docs/releases/v0.5.0/implementation-plan.md",
    "chars": 7162,
    "preview": "# 项目级 Skill 同步功能实现计划\n\n## Context\n\nSkills Hub v0.4.x 只有全局同步。Skill 安装到中央仓库 `~/.skillshub/` 后,再同步到各工具的全局 Skills 目录。\n\nv0.5.0"
  },
  {
    "path": "docs/releases/v0.5.0/project-scope-design.md",
    "chars": 4637,
    "preview": "# 项目级 Skill 同步 — 设计文档\n\n## 背景\n\nSkills Hub v0.4.x 只支持全局同步:Skill 安装到中央仓库后,同步到各工具的全局目录,在所有项目中均可使用。\n\nv0.5.0 新增**项目级同步**:Skill"
  },
  {
    "path": "docs/releases/v0.5.0/ux-optimizations.md",
    "chars": 3273,
    "preview": "# UX 优化记录\n\n收录不需要单独文档的小型 UX 改进。\n\n---\n\n## 关闭按钮改为隐藏窗口(macOS)\n\n**变更:** 点击红色 X 按钮不再退出应用,而是隐藏窗口。\n\n**原因:** macOS 上许多主流应用(Slack、"
  },
  {
    "path": "docs/releases/v0.6.0/minor-updates.md",
    "chars": 650,
    "preview": "# v0.6.0 小需求与体验优化记录\n\n这个文件用于记录 v0.6.0 周期内较小的需求、体验优化和界面修正。后续同类变更继续追加到这里,避免为每个小项单独创建发布记录文件。\n\n## 2026-05-05\n\n### 删除 My Skill"
  },
  {
    "path": "docs/releases/v0.6.0/tag-management.md",
    "chars": 5069,
    "preview": "# Skill 标签管理功能设计与实现计划\n\n## 背景\n\n随着用户安装的 Skill 数量增加,当前列表主要依赖名称、描述和同步状态浏览。用户很难按技术方向或使用场景快速定位 Skill,例如:\n\n- 前端 / React / UI\n- "
  },
  {
    "path": "docs/skills_hub_design.html",
    "chars": 55593,
    "preview": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-wi"
  },
  {
    "path": "docs/skills_hub_v2_design.html",
    "chars": 111764,
    "preview": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-wi"
  },
  {
    "path": "docs/tag_profile_interactive_prototype.html",
    "chars": 76665,
    "preview": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n  <meta charset=\"UTF-8\" />\n  <meta name=\"viewport\" content=\"width=device-widt"
  },
  {
    "path": "eslint.config.js",
    "chars": 636,
    "preview": "import js from '@eslint/js'\nimport globals from 'globals'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport reac"
  },
  {
    "path": "featured-skills.json",
    "chars": 192220,
    "preview": "{\n  \"updated_at\": \"2026-05-13T01:47:46.473Z\",\n  \"total\": 300,\n  \"categories\": [\n    \"ai-assistant\",\n    \"development\"\n  "
  },
  {
    "path": "index.html",
    "chars": 366,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/"
  },
  {
    "path": "package.json",
    "chars": 2763,
    "preview": "{\n  \"name\": \"skills-hub\",\n  \"private\": true,\n  \"version\": \"0.6.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite -"
  },
  {
    "path": "scripts/coverage-rust.sh",
    "chars": 202,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\n\nROOT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")/..\" && pwd)\"\ncd \"$ROOT_DIR/src-tau"
  },
  {
    "path": "scripts/extract-changelog.mjs",
    "chars": 2582,
    "preview": "import fs from 'node:fs'\n\nfunction normalizeVersion(input) {\n  const v = String(input || '').trim()\n  if (!v) return nul"
  },
  {
    "path": "scripts/fetch-featured-skills.mjs",
    "chars": 13264,
    "preview": "#!/usr/bin/env node\n\n/**\n * Aggregates AI Agent Skills from a curated list of high-quality GitHub repositories.\n *\n * Fe"
  },
  {
    "path": "scripts/tauri-icon-desktop.mjs",
    "chars": 1857,
    "preview": "import { spawnSync } from 'node:child_process'\nimport fs from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:"
  },
  {
    "path": "scripts/version.mjs",
    "chars": 5172,
    "preview": "#!/usr/bin/env node\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport process from \"node:process\";\n\nconst R"
  },
  {
    "path": "src/App.css",
    "chars": 53519,
    "preview": ".skills-app {\n  height: 100%;\n  background: var(--bg-app);\n  border: none;\n  border-radius: 0;\n  box-shadow: none;\n  dis"
  },
  {
    "path": "src/App.tsx",
    "chars": 93902,
    "preview": "import { useCallback, useEffect, useMemo, useRef, useState, type MutableRefObject } from 'react'\nimport type { Update } "
  },
  {
    "path": "src/components/Layout.tsx",
    "chars": 1846,
    "preview": "import React from 'react';\nimport { Link, Outlet, useLocation } from 'react-router-dom';\nimport { Home, Settings, Box } "
  },
  {
    "path": "src/components/skills/ExplorePage.tsx",
    "chars": 8198,
    "preview": "import { memo, useMemo } from 'react'\nimport { Plus, Search, Star } from 'lucide-react'\nimport type { TFunction } from '"
  },
  {
    "path": "src/components/skills/FilterBar.tsx",
    "chars": 6575,
    "preview": "import { memo, useEffect, useMemo, useRef, useState } from 'react'\nimport { ArrowUpDown, Check, ChevronDown, Search, Tag"
  },
  {
    "path": "src/components/skills/Header.tsx",
    "chars": 2189,
    "preview": "import { memo } from 'react'\nimport { Layers, Search, Settings, Tag } from 'lucide-react'\nimport type { TFunction } from"
  },
  {
    "path": "src/components/skills/LoadingOverlay.tsx",
    "chars": 1398,
    "preview": "import { memo } from 'react'\nimport type { TFunction } from 'i18next'\n\ntype LoadingOverlayProps = {\n  loading: boolean\n "
  },
  {
    "path": "src/components/skills/SettingsPage.tsx",
    "chars": 11579,
    "preview": "import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { ArrowLeft } from 'lucide-react'"
  },
  {
    "path": "src/components/skills/SkillCard.tsx",
    "chars": 8076,
    "preview": "import { memo, useState } from 'react'\nimport { Box, Copy, Folder, Github, RefreshCw, Tag, Trash2 } from 'lucide-react'\n"
  },
  {
    "path": "src/components/skills/SkillDetailView.tsx",
    "chars": 16828,
    "preview": "import { memo, useCallback, useEffect, useMemo, useState } from 'react'\nimport {\n  ArrowLeft,\n  ChevronDown,\n  ChevronRi"
  },
  {
    "path": "src/components/skills/SkillsList.tsx",
    "chars": 3108,
    "preview": "import { memo } from 'react'\nimport { MessageCircle } from 'lucide-react'\nimport type { TFunction } from 'i18next'\nimpor"
  },
  {
    "path": "src/components/skills/TagsPage.tsx",
    "chars": 4507,
    "preview": "import { memo, useMemo, useState } from 'react'\nimport { ArrowLeft, Plus, Search, Tag } from 'lucide-react'\nimport type "
  },
  {
    "path": "src/components/skills/modals/AddSkillModal.tsx",
    "chars": 7165,
    "preview": "import { memo } from 'react'\nimport { Check } from 'lucide-react'\nimport type { TFunction } from 'i18next'\nimport type {"
  },
  {
    "path": "src/components/skills/modals/DeleteModal.tsx",
    "chars": 1879,
    "preview": "import { memo } from 'react'\nimport { TriangleAlert } from 'lucide-react'\nimport type { TFunction } from 'i18next'\n\ntype"
  },
  {
    "path": "src/components/skills/modals/EditSkillTagsModal.tsx",
    "chars": 3338,
    "preview": "import { memo, useMemo, useState } from 'react'\nimport { Check, Search } from 'lucide-react'\nimport type { TFunction } f"
  },
  {
    "path": "src/components/skills/modals/GitPickModal.tsx",
    "chars": 4383,
    "preview": "import { memo, useMemo, useState } from 'react'\nimport { Search } from 'lucide-react'\nimport type { TFunction } from 'i1"
  },
  {
    "path": "src/components/skills/modals/ImportModal.tsx",
    "chars": 6531,
    "preview": "import { memo, useMemo, useState } from 'react'\nimport { Download, Search } from 'lucide-react'\nimport type { TFunction "
  },
  {
    "path": "src/components/skills/modals/LocalPickModal.tsx",
    "chars": 5334,
    "preview": "import { memo, useMemo, useState } from 'react'\nimport { Search } from 'lucide-react'\nimport type { TFunction } from 'i1"
  },
  {
    "path": "src/components/skills/modals/NewToolsModal.tsx",
    "chars": 1169,
    "preview": "import { memo } from 'react'\nimport type { TFunction } from 'i18next'\n\ntype NewToolsModalProps = {\n  open: boolean\n  loa"
  },
  {
    "path": "src/components/skills/modals/ScopeSyncModal.tsx",
    "chars": 6704,
    "preview": "import { memo, useMemo, useState } from 'react'\nimport { Folder, X } from 'lucide-react'\nimport type { TFunction } from "
  },
  {
    "path": "src/components/skills/modals/SharedDirModal.tsx",
    "chars": 1340,
    "preview": "import { memo } from 'react'\nimport type { TFunction } from 'i18next'\n\ntype SharedDirModalProps = {\n  open: boolean\n  lo"
  },
  {
    "path": "src/components/skills/types.ts",
    "chars": 2177,
    "preview": "export type OnboardingVariant = {\n  tool: string\n  name: string\n  path: string\n  fingerprint?: string | null\n  is_link: "
  },
  {
    "path": "src/i18n/index.ts",
    "chars": 624,
    "preview": "import i18n from 'i18next'\nimport { initReactI18next } from 'react-i18next'\nimport { resources } from './resources'\n\ncon"
  },
  {
    "path": "src/i18n/resources.ts",
    "chars": 27481,
    "preview": "export const resources = {\n  en: {\n    translation: {\n      appName: 'Skills Hub',\n      unknown: 'unknown',\n      langu"
  },
  {
    "path": "src/index.css",
    "chars": 2545,
    "preview": "@import url(\"https://fonts.googleapis.com/css2?family=Fira+Sans:wght@300;400;500;600;700&family=Fira+Code:wght@400;500;6"
  },
  {
    "path": "src/main.tsx",
    "chars": 246,
    "preview": "import { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport './index.css'\nimport './i18n'\nim"
  },
  {
    "path": "src/pages/Dashboard.tsx",
    "chars": 1182,
    "preview": "import { useTranslation } from 'react-i18next';\n\nexport const Dashboard = () => {\n  const { t } = useTranslation();\n  re"
  },
  {
    "path": "src/tauri-plugin-dialog.d.ts",
    "chars": 234,
    "preview": "declare module '@tauri-apps/plugin-dialog' {\n  type OpenDialogOptions = {\n    directory?: boolean\n    multiple?: boolean"
  },
  {
    "path": "src-tauri/.gitignore",
    "chars": 86,
    "preview": "# Generated by Cargo\n# will have compiled files and executables\n/target/\n/gen/schemas\n"
  },
  {
    "path": "src-tauri/Cargo.toml",
    "chars": 1108,
    "preview": "[package]\nname = \"app\"\nversion = \"0.6.0\"\ndescription = \"A Tauri App\"\nauthors = [\"you\"]\nlicense = \"\"\nrepository = \"git@gi"
  },
  {
    "path": "src-tauri/build.rs",
    "chars": 335,
    "preview": "fn main() {\n    // 确保替换图标后,`tauri dev` 的构建会重新触发(否则 Cargo 可能不重跑 build.rs,Dock 仍显示旧图标)。\n    println!(\"cargo:rerun-if-chang"
  },
  {
    "path": "src-tauri/capabilities/default.json",
    "chars": 277,
    "preview": "{\n  \"$schema\": \"../gen/schemas/desktop-schema.json\",\n  \"identifier\": \"default\",\n  \"description\": \"enables the default pe"
  },
  {
    "path": "src-tauri/src/commands/mod.rs",
    "chars": 40680,
    "preview": "use anyhow::Context;\nuse serde::Serialize;\nuse tauri::State;\n\nuse std::sync::Arc;\n\nuse crate::core::cache_cleanup::{\n   "
  },
  {
    "path": "src-tauri/src/commands/tests/commands.rs",
    "chars": 5715,
    "preview": "use super::*;\nuse crate::core::skill_store::SkillRecord;\n\nfn make_store() -> (tempfile::TempDir, SkillStore) {\n    let d"
  },
  {
    "path": "src-tauri/src/core/cache_cleanup.rs",
    "chars": 4520,
    "preview": "use std::path::{Path, PathBuf};\nuse std::time::{Duration, SystemTime};\n\nuse anyhow::{Context, Result};\nuse serde::Deseri"
  },
  {
    "path": "src-tauri/src/core/cancel_token.rs",
    "chars": 1340,
    "preview": "use std::sync::atomic::{AtomicBool, Ordering};\n\n/// Shared cancellation flag for long-running operations.\n/// Managed as"
  },
  {
    "path": "src-tauri/src/core/central_repo.rs",
    "chars": 885,
    "preview": "use std::path::{Path, PathBuf};\n\nuse anyhow::{Context, Result};\nuse dirs::home_dir;\nuse tauri::Manager;\n\nuse super::skil"
  },
  {
    "path": "src-tauri/src/core/content_hash.rs",
    "chars": 1250,
    "preview": "use std::path::Path;\n\nuse anyhow::{Context, Result};\nuse sha2::{Digest, Sha256};\nuse walkdir::{DirEntry, WalkDir};\n\ncons"
  },
  {
    "path": "src-tauri/src/core/featured_skills.rs",
    "chars": 2962,
    "preview": "use anyhow::{Context, Result};\nuse reqwest::blocking::Client;\nuse serde::Deserialize;\n\nuse super::skill_store::SkillStor"
  },
  {
    "path": "src-tauri/src/core/git_fetcher.rs",
    "chars": 17748,
    "preview": "use std::path::Path;\nuse std::process::Command;\nuse std::process::Stdio;\nuse std::sync::OnceLock;\nuse std::time::{Durati"
  },
  {
    "path": "src-tauri/src/core/github_download.rs",
    "chars": 10488,
    "preview": "//! Download a GitHub directory via the Contents API, bypassing git clone entirely.\n//! This is much faster than cloning"
  },
  {
    "path": "src-tauri/src/core/github_search.rs",
    "chars": 2041,
    "preview": "use anyhow::{Context, Result};\nuse reqwest::blocking::Client;\nuse serde::Deserialize;\n\n#[derive(Debug, Deserialize)]\nstr"
  },
  {
    "path": "src-tauri/src/core/installer.rs",
    "chars": 50038,
    "preview": "use std::path::{Path, PathBuf};\nuse std::sync::{Mutex, OnceLock};\n\nuse anyhow::{Context, Result};\nuse serde::{Deserializ"
  },
  {
    "path": "src-tauri/src/core/mod.rs",
    "chars": 353,
    "preview": "pub mod cache_cleanup;\npub mod cancel_token;\npub mod central_repo;\npub mod content_hash;\npub mod featured_skills;\npub mo"
  },
  {
    "path": "src-tauri/src/core/onboarding.rs",
    "chars": 5029,
    "preview": "use std::collections::HashMap;\nuse std::path::{Path, PathBuf};\n\nuse anyhow::Result;\nuse serde::Serialize;\n\nuse super::ce"
  },
  {
    "path": "src-tauri/src/core/skill_files.rs",
    "chars": 2832,
    "preview": "use std::path::Path;\n\nuse anyhow::{bail, Context, Result};\nuse walkdir::{DirEntry, WalkDir};\n\nconst IGNORE_NAMES: [&str;"
  },
  {
    "path": "src-tauri/src/core/skill_store.rs",
    "chars": 25958,
    "preview": "use std::path::{Path, PathBuf};\n\nuse anyhow::{Context, Result};\nuse rusqlite::{params, Connection};\nuse tauri::Manager;\n"
  },
  {
    "path": "src-tauri/src/core/skills_search.rs",
    "chars": 1737,
    "preview": "use anyhow::{Context, Result};\nuse reqwest::blocking::Client;\nuse serde::Deserialize;\n\n#[derive(Debug, Deserialize)]\nstr"
  },
  {
    "path": "src-tauri/src/core/sync_engine.rs",
    "chars": 7020,
    "preview": "use std::path::{Path, PathBuf};\n\nuse anyhow::{Context, Result};\n\n#[allow(dead_code)]\n#[derive(Clone, Debug)]\npub enum Sy"
  },
  {
    "path": "src-tauri/src/core/temp_cleanup.rs",
    "chars": 2362,
    "preview": "use std::path::{Path, PathBuf};\nuse std::time::{Duration, SystemTime};\n\nuse anyhow::{Context, Result};\nuse tauri::Manage"
  },
  {
    "path": "src-tauri/src/core/tests/central_repo.rs",
    "chars": 1012,
    "preview": "use std::path::PathBuf;\n\nuse crate::core::central_repo::{ensure_central_repo, resolve_central_repo_path};\nuse crate::cor"
  },
  {
    "path": "src-tauri/src/core/tests/content_hash.rs",
    "chars": 713,
    "preview": "use std::fs;\n\nuse crate::core::content_hash::hash_dir;\n\n#[test]\nfn hash_changes_with_content_and_ignores_git_dir() {\n   "
  },
  {
    "path": "src-tauri/src/core/tests/featured_skills.rs",
    "chars": 3084,
    "preview": "use super::fetch_featured_skills_inner;\nuse crate::core::skill_store::SkillStore;\n\nfn temp_store() -> SkillStore {\n    l"
  },
  {
    "path": "src-tauri/src/core/tests/git_fetcher.rs",
    "chars": 2816,
    "preview": "use std::fs;\n\nuse crate::core::git_fetcher::{clone_or_pull, clone_or_pull_sparse};\n\nfn commit_file(repo: &git2::Reposito"
  },
  {
    "path": "src-tauri/src/core/tests/github_search.rs",
    "chars": 2442,
    "preview": "use mockito::Matcher;\n\nuse super::search_github_repos_inner;\n\nfn json_one_repo() -> String {\n    r#\"{\n  \"items\": [\n    {"
  },
  {
    "path": "src-tauri/src/core/tests/installer.rs",
    "chars": 26693,
    "preview": "use std::fs;\nuse std::path::{Path, PathBuf};\n\nuse crate::core::skill_store::{SkillRecord, SkillStore, SkillTargetRecord}"
  },
  {
    "path": "src-tauri/src/core/tests/onboarding.rs",
    "chars": 2528,
    "preview": "use std::fs;\n\nuse super::build_onboarding_plan_in_home;\n\n#[test]\nfn groups_by_name_and_detects_conflicts_by_fingerprint("
  },
  {
    "path": "src-tauri/src/core/tests/skill_store.rs",
    "chars": 15371,
    "preview": "use std::path::PathBuf;\n\nuse crate::core::skill_store::{SkillRecord, SkillStore, SkillTargetRecord};\nuse rusqlite::Conne"
  },
  {
    "path": "src-tauri/src/core/tests/skills_search.rs",
    "chars": 2868,
    "preview": "use mockito::Matcher;\n\nuse super::search_skills_online_inner;\n\nfn json_response() -> String {\n    r#\"{\n  \"skills\": [\n   "
  },
  {
    "path": "src-tauri/src/core/tests/sync_engine.rs",
    "chars": 3482,
    "preview": "use std::fs;\n\nuse crate::core::sync_engine::{\n    copy_dir_recursive, sync_dir_for_tool_with_overwrite, sync_dir_hybrid,"
  },
  {
    "path": "src-tauri/src/core/tests/temp_cleanup.rs",
    "chars": 760,
    "preview": "use std::fs;\nuse std::time::Duration;\n\nuse super::{cleanup_old_git_temp_dirs_in, mark_temp_dir};\n\n#[test]\nfn cleanup_rem"
  },
  {
    "path": "src-tauri/src/core/tests/tool_adapters.rs",
    "chars": 5404,
    "preview": "use std::fs;\n\nuse crate::core::tool_adapters::{\n    adapter_by_key, adapters_sharing_project_skills_dir, adapters_sharin"
  },
  {
    "path": "src-tauri/src/core/tool_adapters/mod.rs",
    "chars": 19965,
    "preview": "use std::path::{Path, PathBuf};\n\nuse anyhow::{Context, Result};\n\n#[derive(Clone, Debug, PartialEq, Eq)]\npub enum ToolId "
  },
  {
    "path": "src-tauri/src/lib.rs",
    "chars": 5158,
    "preview": "mod commands;\nmod core;\n\nuse std::sync::Arc;\n\nuse core::cancel_token::CancelToken;\nuse core::skill_store::{default_db_pa"
  },
  {
    "path": "src-tauri/src/main.rs",
    "chars": 179,
    "preview": "// Prevents additional console window on Windows in release, DO NOT REMOVE!!\n#![cfg_attr(not(debug_assertions), windows_"
  },
  {
    "path": "src-tauri/tauri.conf.json",
    "chars": 1188,
    "preview": "{\n  \"$schema\": \"../node_modules/@tauri-apps/cli/config.schema.json\",\n  \"productName\": \"Skills Hub\",\n  \"version\": \"0.6.0\""
  },
  {
    "path": "tsconfig.app.json",
    "chars": 732,
    "preview": "{\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.app.tsbuildinfo\",\n    \"target\": \"ES2023\",\n"
  },
  {
    "path": "tsconfig.json",
    "chars": 119,
    "preview": "{\n  \"files\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.app.json\" },\n    { \"path\": \"./tsconfig.node.json\" }\n  ]\n}\n"
  },
  {
    "path": "tsconfig.node.json",
    "chars": 653,
    "preview": "{\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.node.tsbuildinfo\",\n    \"target\": \"ES2023\","
  },
  {
    "path": "vite.config.ts",
    "chars": 275,
    "preview": "import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\nimport tailwindcss from '@tailwindcss/vite'"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the qufei1993/skills-hub GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 124 files (1.1 MB), approximately 329.9k tokens, and a symbol index with 453 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!