From 85a7070a826e3c09068f831506ec96fc577f0d35 Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sun, 30 Nov 2025 08:36:46 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 11 ++ README.md | 3 + commands/install-skill.md | 329 +++++++++++++++++++++++++++++++++++++ commands/remove-skill.md | 176 ++++++++++++++++++++ commands/update-skill.md | 161 ++++++++++++++++++ plugin.lock.json | 53 ++++++ 6 files changed, 733 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 commands/install-skill.md create mode 100644 commands/remove-skill.md create mode 100644 commands/update-skill.md create mode 100644 plugin.lock.json diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..093e8b5 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,11 @@ +{ + "name": "local-install-skill", + "description": "Install Claude Code skills from GitHub repositories to project scope", + "version": "1.0.0", + "author": { + "name": "lagz0ne" + }, + "commands": [ + "./commands" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4c01418 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# local-install-skill + +Install Claude Code skills from GitHub repositories to project scope diff --git a/commands/install-skill.md b/commands/install-skill.md new file mode 100644 index 0000000..394022b --- /dev/null +++ b/commands/install-skill.md @@ -0,0 +1,329 @@ +--- +name: install-skill +description: Install a skill from GitHub to project scope (.claude/skills/) +arguments: + - name: source + description: "GitHub repo with optional branch and skill path: user/repo, user/repo:skill-name, user/repo#branch:skill-name, or user/repo --all" + required: true + - name: mode + description: "Installation mode: --copy (files only) or --submodule (git submodule, default)" + required: false +--- + +# Install Skill from GitHub + +You are installing a Claude Code skill from a GitHub repository to project scope. + +## Parse the Input + +Parse the source argument: `$ARGUMENTS.source` + +Supported formats: +- `user/repo` - Clone repo, look for skill at root or in `skills/` directory +- `user/repo:skill-name` - Install specific skill from `skills//` +- `user/repo#branch` - Use specific branch +- `user/repo#branch:skill-name` - Branch + specific skill +- `user/repo --all` - Install all skills found in repo + +Extract: +- `owner`: GitHub username/org +- `repo`: Repository name +- `branch`: Branch name (default: repo's default branch) +- `skillPath`: Path to skill within repo (optional) +- `installAll`: Boolean flag for --all + +## Determine Installation Mode + +Check for mode flags in `$ARGUMENTS.mode` or within `$ARGUMENTS.source`: + +**Flag detection:** + +1. First check `$ARGUMENTS.mode`: + - If `--copy` or `copy`: `mode = "copy"` + - If `--submodule` or `submodule`: `mode = "submodule"` + +2. If not found, check if flag is appended to source string: + - Split `$ARGUMENTS.source` by spaces + - If last token is `--copy`: `mode = "copy"`, remove flag from source + - If last token is `--submodule`: `mode = "submodule"`, remove flag from source + +3. If no flag found, prompt user: + +**Interactive prompt (when no flag):** + +``` +Use AskUserQuestion: + question: "How would you like to install this skill?" + header: "Install Mode" + multiSelect: false + options: + - label: "Submodule (recommended)" + description: "Full repo as git submodule, easy updates via git submodule update" + - label: "Copy" + description: "Only skill files copied, minimal footprint, re-run install to update" +``` + +Map user response to mode variable: +- "Submodule (recommended)" → `mode = "submodule"` +- "Copy" → `mode = "copy"` + +**Default:** If user doesn't respond or cancels, default to `mode = "submodule"`. + +The `mode` variable (value: `"copy"` or `"submodule"`) is used in subsequent installation steps. + +## Installation Process + +### Step 1: Setup directories + +```bash +mkdir -p .claude/skills +``` + +### Step 2: Find skills to install + +If `--all` flag: +- Look for `skills/` directory in cloned repo +- Find all subdirectories containing `SKILL.md` +- Collect list of skill names + +If specific skill path: +- Check if `skills//SKILL.md` exists +- If not, check if `/SKILL.md` exists at root +- If not found, report error with helpful message + +If no skill specified: +- Check if repo root has `SKILL.md` (repo IS the skill) +- If not, check if `skills/` directory exists and has exactly one skill +- If multiple skills found, list them and ask user to specify + +### Step 3: Validate each skill + +For each skill to install: +1. Verify `SKILL.md` exists +2. Read frontmatter and validate: + - `name` field exists (max 64 chars) + - `description` field exists (max 1024 chars) +3. Warn if validation fails but continue + +### Step 4: Execute Installation Based on Mode + +#### If mode is "copy": + +**2a. Clone to temporary directory:** +```bash +TEMP_DIR=$(mktemp -d) +git clone --depth=1 https://github.com//.git "$TEMP_DIR" +``` + +If specific branch: +```bash +git clone --depth=1 --branch https://github.com//.git "$TEMP_DIR" +``` + +**2b. Get commit SHA before cleanup:** +```bash +COMMIT_SHA=$(git -C "$TEMP_DIR" rev-parse HEAD) +``` + +**2c. Copy skill directory:** +```bash +mkdir -p .claude/skills +cp -r "$TEMP_DIR/" ".claude/skills/" +``` + +**2d. Clean up temp directory:** +```bash +rm -rf "$TEMP_DIR" +``` + +#### If mode is "submodule": + +**2a. Check if this is a git repository:** +```bash +git rev-parse --git-dir > /dev/null 2>&1 +``` + +If not a git repo, error: +``` +Error: Submodule mode requires a git repository. + +Either initialize git first (git init) or use --copy mode. +``` + +**2b. Check if submodule already exists:** +```bash +if [ -d ".claude/submodules/" ]; then + # Verify it's the same source + EXISTING_URL=$(git config --file .gitmodules submodule..claude/submodules/.url) + EXPECTED_URL="https://github.com//.git" + + if [ "$EXISTING_URL" = "$EXPECTED_URL" ]; then + # Same URL - use existing submodule, skip git submodule add + echo "Submodule already exists with matching URL, using existing" + else + # Different URL - error + echo "Error: Submodule exists but URL differs" + echo " Existing: $EXISTING_URL" + echo " Expected: $EXPECTED_URL" + exit 1 + fi +fi +``` + +**2c. Add submodule if not exists:** + +Skip if submodule already exists with matching URL (from step 2b). + +Otherwise: +```bash +mkdir -p .claude/submodules +git submodule add https://github.com//.git .claude/submodules/ +``` + +If specific branch: +```bash +git submodule add -b https://github.com//.git .claude/submodules/ +``` + +**2d. Get commit SHA:** +```bash +COMMIT_SHA=$(git -C ".claude/submodules/" rev-parse HEAD) +``` + +### Step 5: Create symlinks (submodule mode only) + +**Skip this step if mode is "copy"** - files are already in `.claude/skills//`. + +For submodule mode, create symlink: + +Check if `.claude/skills/` already exists: +- If symlink pointing to same target: skip, already installed +- If symlink pointing elsewhere or regular directory: ask user to overwrite or skip + +Create relative symlink: +```bash +mkdir -p .claude/skills +ln -s ../submodules// .claude/skills/ +``` + +For example, if skill is at `skills/brainstorming/`: +```bash +ln -s ../submodules/superpowers/skills/brainstorming .claude/skills/brainstorming +``` + +### Step 6: Update registry + +Read or create `.claude/local-plugins.yaml`: + +```yaml +version: 2 +skills: {} +submodules: {} +``` + +**Migration:** If `.claude/local-plugins.json` exists (v1), migrate it first: +1. Read JSON content +2. Convert each repo's skills to v2 format with `mode: legacy` +3. Write to `.claude/local-plugins.yaml` +4. Delete `.claude/local-plugins.json` + +**Add skill entry:** + +```yaml +skills: + : + mode: + source: github.com// + repo: + branch: + skillPath: + installedAt: + commitSha: +``` + +**If submodule mode, also update submodules section:** + +```yaml +submodules: + : + source: github.com// + path: .claude/submodules/ + skills: + - +``` + +If repo already in submodules, just append skill name to the `skills` list. + +**Write YAML:** Use proper YAML formatting with 2-space indentation. + +### Step 7: Update .gitignore (copy mode only) + +**Skip this step for submodule mode** - submodules are tracked by git naturally. + +For copy mode, the skill files in `.claude/skills//` will be committed directly. No .gitignore changes needed. + +**Note:** The old `.claude/plugins/local/` pattern can be removed from .gitignore if present, as we no longer use that directory. + +### Step 8: Report success + +**Copy mode output:** +``` +Installing skill from github.com//... +├── Mode: Copy +├── Downloading: +├── Copying to: .claude/skills/ +└── Status: Installed successfully + +Skill "" is now available in this project. +``` + +**Submodule mode output:** +``` +Installing skill from github.com//... +├── Mode: Submodule +├── Adding submodule: .claude/submodules/ +├── Creating symlink: .claude/skills/ +└── Status: Installed successfully + +Skill "" is now available in this project. + +Note: Team members should run `git submodule update --init` after cloning. +``` + +## Error Handling + +**Repository not found:** +``` +Error: Repository not found: github.com// + +Check the repository URL and ensure it's public or you have access. +``` + +**No SKILL.md found:** +``` +Error: No SKILL.md found at /: + +Expected structure: + skill-name/ + └── SKILL.md (required) + +Available skills in this repo: + - skill-a (skills/skill-a/) + - skill-b (skills/skill-b/) + +Try: /install-skill /:skill-a +``` + +**Network error:** +``` +Error: Failed to clone repository + +Check your network connection and try again. +``` + +## Notes + +- Always use relative symlinks so they work when repo is moved +- The `.claude/plugins/local/` directory should NOT be committed (add to .gitignore) +- The `.claude/skills/` symlinks SHOULD be committed (team sharing) +- Team members need to run `/install-skill` after cloning to populate local repos diff --git a/commands/remove-skill.md b/commands/remove-skill.md new file mode 100644 index 0000000..33d5d31 --- /dev/null +++ b/commands/remove-skill.md @@ -0,0 +1,176 @@ +--- +name: remove-skill +description: Remove a locally installed skill +arguments: + - name: skill_name + description: "Name of the skill to remove" + required: true +--- + +# Remove Locally Installed Skill + +You are removing a Claude Code skill that was installed via `/install-skill`. + +## Parse the Input + +- `skill_name`: `$ARGUMENTS.skill_name` - The skill to remove + +## Removal Process + +### Step 1: Read registry and find skill + +Read `.claude/local-plugins.yaml`. + +Look up `skills.`. If not found: +``` +Error: Skill "" not found in registry. + +Installed skills: + - skill-a (copy) + - skill-b (submodule) + +Use the exact skill name from the list above. +``` + +Extract the skill's `mode` for appropriate removal. + +### Step 2: Remove skill files/symlink + +#### If mode is "copy": + +```bash +rm -rf ".claude/skills/" +``` + +#### If mode is "submodule": + +```bash +rm ".claude/skills/" # Remove symlink only +``` + +### Step 3: Update registry - skills section + +Remove the skill entry from `skills` section. + +### Step 4: Handle submodule cleanup (submodule mode only) + +**Skip if mode is "copy".** + +Check if other skills still use this submodule: +1. Read `submodules..skills` list +2. Remove `` from the list +3. If list is now empty, remove the submodule: + +```bash +git submodule deinit .claude/submodules/ +git rm .claude/submodules/ +rm -rf .git/modules/.claude/submodules/ +``` + +Remove the repo entry from `submodules` section. + +If other skills still use the submodule: +``` +Note: Submodule .claude/submodules/ retained. +Other skills still using it: skill-x, skill-y +``` + +### Step 5: Clean up empty directories + +```bash +# Remove empty skills directory if no skills left +rmdir .claude/skills 2>/dev/null || true + +# Remove empty submodules directory if no submodules left +rmdir .claude/submodules 2>/dev/null || true +``` + +### Step 6: Write updated registry + +Write `.claude/local-plugins.yaml`. + +If no skills remain, consider removing the file: +```bash +if [ -z "$(grep -v '^version:' .claude/local-plugins.yaml | grep -v '^skills: {}' | grep -v '^submodules: {}')" ]; then + rm .claude/local-plugins.yaml +fi +``` + +### Step 7: Report success + +**Copy mode removal:** +``` +Removing skill ""... +├── Mode: Copy +├── Removed: .claude/skills// +└── Registry updated + +Skill "" has been removed. +``` + +**Submodule mode removal (submodule kept):** +``` +Removing skill ""... +├── Mode: Submodule +├── Removed symlink: .claude/skills/ +├── Submodule retained: .claude/submodules/ +│ └── Other skills: skill-x, skill-y +└── Registry updated + +Skill "" has been removed. +``` + +**Submodule mode removal (submodule removed):** +``` +Removing skill ""... +├── Mode: Submodule +├── Removed symlink: .claude/skills/ +├── Removed submodule: .claude/submodules/ +└── Registry updated + +Skill "" and its submodule have been removed. +``` + +## List Installed Skills + +If user runs `/remove-skill` without arguments or with `--list`: + +``` +Locally installed skills: + +Copy mode: + - my-custom-skill (from github.com/user/repo) + +Submodule mode: + - brainstorming (from github.com/obra/superpowers) + - tdd (from github.com/obra/superpowers) + +Use: /remove-skill +``` + +## Error Handling + +**Skill files missing but in registry:** +``` +Warning: Skill "" in registry but files not found. +Cleaning up registry entry... + +Done. Registry cleaned. +``` + +**Permission error:** +``` +Error: Permission denied removing .claude/skills/ + +Check file permissions and try again. +``` + +**Submodule removal fails:** +``` +Error: Failed to remove submodule .claude/submodules/ + +Manual cleanup may be required: + git submodule deinit .claude/submodules/ + git rm .claude/submodules/ + rm -rf .git/modules/.claude/submodules/ +``` diff --git a/commands/update-skill.md b/commands/update-skill.md new file mode 100644 index 0000000..ee677f8 --- /dev/null +++ b/commands/update-skill.md @@ -0,0 +1,161 @@ +--- +name: update-skill +description: Update locally installed skills from their GitHub repositories +arguments: + - name: target + description: "Repository name to update, or --all to update all repos" + required: false +--- + +# Update Locally Installed Skills + +You are updating Claude Code skills that were installed via `/install-skill`. + +## Parse the Input + +Parse the target argument: `$ARGUMENTS.target` + +- If empty or not provided: List installed skills and ask which to update +- If `--all`: Update all updateable skills +- Otherwise: Update the specified skill or repo + +## Update Process + +### Step 1: Read registry + +Read `.claude/local-plugins.yaml`. + +**Migration check:** If `.claude/local-plugins.json` exists but `.yaml` doesn't, run migration first. + +If no registry found: +``` +No locally installed skills found. + +Use /install-skill to install skills from GitHub. +``` + +### Step 2: Identify what to update + +**If `--all`:** +- Get all skills from registry +- Group by mode for appropriate update method + +**If specific target:** +- Check if it's a skill name in `skills` section +- Check if it's a repo name in `submodules` section +- If not found, list available options + +**If no target:** +- List all skills with their mode and source +- Ask user which to update + +### Step 3: Update based on mode + +#### For submodule-based skills: + +Update the entire submodule (updates all skills from that repo): + +Capture old and new SHA: +```bash +# BEFORE fetch/pull +OLD_SHA=$(git -C .claude/submodules/ rev-parse HEAD) + +# Fetch and pull +git -C .claude/submodules/ fetch origin +git -C .claude/submodules/ pull origin + +# AFTER pull +NEW_SHA=$(git -C .claude/submodules/ rev-parse HEAD) +``` + +Update all skills from this repo in registry with new `commitSha`. + +#### For copy-based skills: + +Re-download and overwrite: + +```bash +TEMP_DIR=$(mktemp -d) +git clone --depth=1 --branch https://github.com//.git "$TEMP_DIR" +NEW_SHA=$(git -C "$TEMP_DIR" rev-parse HEAD) +``` + +Compare with stored `commitSha`. If same: +``` +Skill "" is already up to date (sha: ) +``` + +If different: +```bash +rm -rf ".claude/skills/" +cp -r "$TEMP_DIR/" ".claude/skills/" +rm -rf "$TEMP_DIR" +``` + +Update `commitSha` in registry. + +### Step 4: Verify integrity + +**For submodule skills:** +- Check symlinks still point to valid targets +- If skill was removed from upstream repo, warn user + +**For copy skills:** +- Verify files were copied successfully + +### Step 5: Update registry + +Write updated `.claude/local-plugins.yaml` with new `commitSha` values. + +### Step 6: Report results + +**No changes:** +``` +Updating ... +└── Already up to date (sha: abc123) +``` + +**Updated (submodule):** +``` +Updating submodule ... +├── Previous: abc123 +├── Updated to: def456 +├── Skills updated: brainstorming, tdd +└── Symlinks verified: OK + +Skills updated successfully. +``` + +**Updated (copy):** +``` +Updating skill ... +├── Previous: abc123 +├── Updated to: def456 +├── Files replaced in: .claude/skills/ +└── Status: Updated successfully +``` + +## Error Handling + +**Submodule directory missing:** +``` +Error: Submodule not found: .claude/submodules/ + +Run `git submodule update --init` or reinstall with /install-skill. +``` + +**Network error:** +``` +Error: Failed to fetch updates + +Check your network connection and try again. +``` + +**Skill not in registry:** +``` +Error: Skill "" not found in registry. + +Installed skills: + - skill-a (copy mode) + - skill-b (submodule mode) +``` diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..cd74d45 --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,53 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:lagz0ne/local-install-skill:", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "f24b9afa1862d79aa7f539cf94bf26ce642d397c", + "treeHash": "2f76f6f0a7f7e4583e66b5059c1ce7b3274f286385b151715220afb6dfb3f86b", + "generatedAt": "2025-11-28T10:20:03.201634Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "local-install-skill", + "description": "Install Claude Code skills from GitHub repositories to project scope", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "7954e2e78b9072bbb0a0e9d6b90988607da269c815cfce0f2d2303e4b7b19b19" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "3df0e6c6d065a86a011735a8f64b6fe280353024eb08c9869b29fe86655a19d7" + }, + { + "path": "commands/install-skill.md", + "sha256": "bafb1faef194050303835d4dfe6656d0b48f66e0d0801c79a52446ba812ef827" + }, + { + "path": "commands/remove-skill.md", + "sha256": "12381afa8b155bc35da1b22b7b7b8739fce6b64918f87936c3d7e0895f8ff111" + }, + { + "path": "commands/update-skill.md", + "sha256": "f0717a6dafc0853cfdf4e39631352a3b211e54aed3164e5311b9331c7a86331f" + } + ], + "dirSha256": "2f76f6f0a7f7e4583e66b5059c1ce7b3274f286385b151715220afb6dfb3f86b" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file