From b2589e50293f8466ef004938b440b2a7f9b61a95 Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sat, 29 Nov 2025 18:01:42 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 17 + README.md | 3 + commands/apply-template.md | 222 +++++++++ commands/finish-worktree.md | 575 +++++++++++++++++++++++ hooks/hooks.json | 16 + hooks/scripts/eslint-fix.cjs | 136 ++++++ plugin.lock.json | 69 +++ skills/git-commit-messages/SKILL.md | 145 ++++++ skills/git-directory-management/SKILL.md | 209 ++++++++ skills/skill-rules.json | 48 ++ 10 files changed, 1440 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 commands/apply-template.md create mode 100644 commands/finish-worktree.md create mode 100644 hooks/hooks.json create mode 100755 hooks/scripts/eslint-fix.cjs create mode 100644 plugin.lock.json create mode 100644 skills/git-commit-messages/SKILL.md create mode 100644 skills/git-directory-management/SKILL.md create mode 100644 skills/skill-rules.json diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..56c872c --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,17 @@ +{ + "name": "tools", + "description": "Skills and documentation for various CLI, development, and language-specific tools", + "version": "1.0.0", + "author": { + "name": "boneskull" + }, + "skills": [ + "./skills" + ], + "commands": [ + "./commands" + ], + "hooks": [ + "./hooks" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6ff4971 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# tools + +Skills and documentation for various CLI, development, and language-specific tools diff --git a/commands/apply-template.md b/commands/apply-template.md new file mode 100644 index 0000000..c98798b --- /dev/null +++ b/commands/apply-template.md @@ -0,0 +1,222 @@ +--- +description: Retroactively apply configuration and dev dependencies from boneskull-template to an existing project +argument-hint: [target-directory] +--- + +# /apply-template + +## Purpose + +Retroactively apply configuration files and development dependencies from the [boneskull-template](https://github.com/boneskull/boneskull-template) repository to an existing project, intelligently merging package.json and copying missing configuration files. + +## Contract + +**Inputs:** `target-directory` (optional) — Target project directory (defaults to current working directory) +**Outputs:** Summary of changes made + +## Instructions + +### 1. Preparation + +- Validate target directory exists and contains a `package.json` +- Verify git working tree is clean in target (warn user if not) +- Fetch or clone latest boneskull-template: + - **Cache location:** `~/.cache/boneskull-template` + - If cache exists: `cd ~/.cache/boneskull-template && git pull origin main` + - If cache doesn't exist: `git clone https://github.com/boneskull/boneskull-template.git ~/.cache/boneskull-template` + - This avoids repeated clones and speeds up subsequent runs + +### 2. Ignore List + +**Never copy these from template:** + +- `docs/plans/` +- `src/` +- `test/` +- `package-lock.json` +- Any file that already exists in target project (EXCEPT `package.json`) + +### 3. Merge package.json + +**Strategy:** Use the `merge-package.js` script to intelligently merge dependencies + +1. **Create backup:** `cp package.json package.json.backup` +2. **Run merge script:** + + ```bash + node plugins/tools/scripts/merge-package.js \ + /package.json \ + ~/.cache/boneskull-template/package.json + ``` + +3. **Install dependencies:** Run `npm install` to install newly added dependencies +4. **Clean up:** Delete `package.json.backup` after successful merge and install + +The merge script automatically: + +- Compares versions between template and target +- Chooses the most recent semantic version +- Adds any dependencies that exist only in template +- Keeps any dependencies that exist only in target +- Adds scripts from template that don't exist in target (preserves user customizations) +- Adds missing fields like `engines`, `knip`, `lint-staged`, etc. +- Merges prettier config and adds plugins + +**Version comparison logic (handled by merge-package.js):** + +```javascript +// Use semver comparison - choose higher version +// Examples: +// "1.2.3" vs "1.2.4" → choose "1.2.4" +// "^1.0.0" vs "^2.0.0" → choose "^2.0.0" +// "~1.2.3" vs "1.2.4" → choose "1.2.4" (prefer exact) +// "latest" vs "1.0.0" → choose "latest" +``` + +### 4. Copy Configuration Files + +**Strategy:** Copy files from template only if they don't exist in target + +1. Get list of all files in template root (excluding ignored paths) +2. For each file: + - Skip if in ignore list + - Skip if already exists in target + - Copy to target if missing +3. Handle `.github/` directory specially: + - Copy `.github/` files that don't exist in target + - Don't overwrite existing workflow files + - Preserve target's existing .github structure + +**Files to copy (if missing):** + +- `.editorconfig` +- `.gitattributes` +- `.gitignore` (careful - may want to merge) +- `.eslintrc.*` / `eslint.config.js` +- `.prettierrc.*` / `prettier.config.js` +- `.prettierignore` +- `.commitlintrc.*` +- `.markdownlint*` +- `.npmrc` +- `tsconfig.json` +- `cspell.json` +- `.husky/` directory and contents +- `.github/` directory (non-overlapping files only) +- `LICENSE` (only if missing) +- Other dotfiles in root + +### 5. Post-Application Steps + +After all changes are complete, inform user they should: + +1. **Review changes:** + + ```bash + git diff package.json + git status # See new files + ``` + +2. **Initialize tools if needed:** + + ```bash + # If Husky was added: + npm run prepare # or: npx husky install + ``` + +3. **Review and customize:** + - Check new configuration files match project needs + - Adjust scripts in package.json + - Customize ESLint/Prettier rules + - Update README with new tooling info + +**Note:** Dependencies are automatically installed during step 3 (after merging package.json), so no separate `npm install` is needed unless the user wants to run it again. + +### 6. Output Format + +Provide clear summary of actions taken: + +```text +✅ Applied boneskull-template to project + +Package.json changes: + 📦 Added dependencies: prettier, eslint, typescript + 📦 Updated dependencies: husky (8.0.0 → 9.0.0), lint-staged (15.0.0 → 16.0.0) + 📝 Added scripts: lint, format, test + +Configuration files added: + ✨ .editorconfig + ✨ .prettierrc.json + ✨ eslint.config.js + ✨ tsconfig.json + ✨ .husky/pre-commit + ✨ .github/workflows/ci.yml + +Files skipped (already exist): + ⏭️ .gitignore + ⏭️ LICENSE + ⏭️ .github/workflows/release.yml + +Next steps: + 1. Review changes: git diff package.json && git status + 2. Initialize Husky: npm run prepare + 3. Customize configs as needed +``` + +## Example Usage + +```bash +# Apply to current directory +/tools:apply-template + +# Apply to specific project +/tools:apply-template ../my-project + +# Apply to absolute path +/tools:apply-template /Users/me/projects/my-app +``` + +## Constraints + +- **Never overwrite existing files** (except `package.json`) +- **Always choose newer version** when merging dependencies +- **Preserve user customizations** in scripts and configs +- **Git working tree must be clean** (warn and exit if not) +- **Validate package.json** after merge (must be valid JSON) +- **Create backup** of original package.json before modifying +- **Handle errors gracefully** (missing template, network issues, etc.) + +## Edge Cases + +1. **Git not clean:** + - Warn user: "Working tree has uncommitted changes. Commit or stash before applying template." + - Exit without making changes + +2. **No package.json in target:** + - Error: "Target directory is not a Node.js project (no package.json found)" + - Exit + +3. **Network error fetching template:** + - Try local cached copy if available + - Error if no cached copy: "Cannot fetch template. Check network connection." + +4. **Version comparison ambiguity:** + - If versions are equivalent (e.g., "^1.0.0" vs "~1.0.2"), prefer exact version + - If can't parse version, keep target's version and warn user + +5. **.gitignore conflicts:** + - If target has .gitignore, DON'T overwrite + - Consider offering to merge (show diff, ask user) + +## Implementation Notes + +- **Template cache:** `~/.cache/boneskull-template` +- **Template URL:** `https://github.com/boneskull/boneskull-template.git` +- **Merge script:** `plugins/tools/scripts/merge-package.js` - handles all package.json merging logic +- **Workflow:** + 1. Ensure template cache exists (clone if needed, pull if exists) + 2. Create package.json.backup + 3. Run merge-package.js script + 4. Run npm install + 5. Copy missing configuration files + 6. Delete package.json.backup + 7. Display summary of changes diff --git a/commands/finish-worktree.md b/commands/finish-worktree.md new file mode 100644 index 0000000..6c1cf8f --- /dev/null +++ b/commands/finish-worktree.md @@ -0,0 +1,575 @@ +--- +description: Merge a finished feature branch from a worktree while maintaining linear history +argument-hint: [main-worktree-path] +--- + +# /finish-worktree + +## Purpose + +Merge a completed feature branch from a git worktree into main using rebase and fast-forward merge to maintain a linear commit history. This command ensures no merge commits are created. + +## Contract + +**Inputs:** `main-worktree-path` (optional) — Path to the main worktree directory (prompts user if not provided) +**Outputs:** Summary of merge operation and next steps + +## Instructions + +### 1. Validate Current State + +**Before starting:** + +- Verify you're currently in a git worktree (not the main repository) +- Check that working tree is clean: `git status` +- If working tree has uncommitted changes, warn user and exit: + + ```text + ⚠️ Working tree has uncommitted changes. Please commit or stash before finishing the worktree. + ``` + +- Get the current feature branch name: `git branch --show-current` + +### 2. Rebase Feature Branch onto Main + +**Goal:** Replay feature branch commits on top of latest main + +1. **Detect if rebase is already in progress:** + + ```bash + test -d .git/rebase-merge || test -d .git/rebase-apply + ``` + + **If rebase IS in progress:** + - Skip to step 3 (conflict handling) + - Do NOT attempt to start a new rebase + + **If rebase is NOT in progress:** + - Continue to step 2 (fetch and start rebase) + +2. **Fetch latest main (only if no rebase in progress):** + + ```bash + git fetch origin main:main + ``` + +3. **Start rebase (only if no rebase in progress):** + + ```bash + git rebase main + ``` + +4. **Handle rebase conflicts:** + + **If rebase completed successfully:** + - Proceed to step 3 (Navigate to Main Worktree) + + **If conflicts exist:** + - List conflicting files: `git status --short | grep '^UU'` + - Instruct user to resolve conflicts: + + ```text + ⚠️ Rebase conflicts detected in: + + + Please resolve these conflicts and run: + git add + git rebase --continue + + Then call /tools:finish-worktree again to continue. + ``` + + - Exit and wait for user to resolve conflicts + + **If conflicts were resolved but rebase hasn't continued:** + - Check status: `git status` + - If status shows "all conflicts fixed: run 'git rebase --continue'": + + ```text + ✅ Conflicts resolved but rebase not continued. + + Please run: + git rebase --continue + + Then call /tools:finish-worktree again to continue. + ``` + + - Exit and wait for user to continue rebase + +5. **Repeat conflict resolution:** + - After user runs `git rebase --continue`, conflicts may occur again + - Repeat step 4 until rebase completes + - **Critical:** Never use `git rebase --skip` — every commit must be preserved + - **Critical:** Never alter commit messages unless user explicitly requests it + +6. **Verify rebase completion:** + + ```bash + git status + ``` + + - Ensure output shows "nothing to commit, working tree clean" + - Ensure no rebase in progress (no `.git/rebase-merge` or `.git/rebase-apply` directory) + - Only proceed to step 3 once rebase is fully complete + +### 3. Navigate to Main Worktree + +**Goal:** Switch to the worktree containing the main branch + +1. **Check for main worktree argument:** + - If `main-worktree-path` argument was provided, use it + - If not provided, attempt to auto-detect: + + ```bash + git worktree list | grep 'main]$' + ``` + + - If auto-detection finds exactly one main worktree, use its path + - If auto-detection fails or finds multiple, prompt user: + + ```text + 📍 Please provide the path to your main worktree directory. + + Hint: Run `git worktree list` to see all worktrees. + ``` + +2. **Validate main worktree path:** + - Check directory exists + - Check it's a git repository: `test -d /.git || test -f /.git` + - Navigate to directory: `cd ` + - Verify branch is main: `git branch --show-current` should output "main" + - If not on main branch, error and exit: + + ```text + ❌ Error: Directory is not on main branch (currently on: ) + Please provide the correct path to the main worktree. + ``` + +3. **Ensure main is clean:** + - Check status: `git status` + - If uncommitted changes exist, warn and exit: + + ```text + ⚠️ Main worktree has uncommitted changes. Please commit or stash before finishing. + ``` + +### 4. Fast-Forward Main to Feature Branch + +**Goal:** Update main to point to the rebased feature branch + +**Critical constraint:** This operation must be a fast-forward. Any divergence indicates history was not properly rebased. + +1. **Attempt fast-forward merge:** + + ```bash + git merge --ff-only + ``` + +2. **Handle fast-forward outcomes:** + + **Success (exit code 0):** + - Proceed to step 5 (Delete Feature Worktree) + + **Failure (non-zero exit code):** + - Check if feature branch doesn't exist locally: + + ```bash + git show-ref --verify --quiet refs/heads/ + ``` + + - If branch doesn't exist, it might be in the worktree only + - Attempt to reference it from worktree: + + ```bash + # Get the commit SHA from the feature worktree + FEATURE_SHA=$(git rev-parse 2>/dev/null || \ + cd && git rev-parse HEAD) + git merge --ff-only $FEATURE_SHA + ``` + + - If still fails, this indicates history divergence: + + ```text + ❌ Error: Cannot fast-forward main to feature branch. + + This usually means: + 1. The feature branch was not properly rebased onto main, OR + 2. Main has new commits since the rebase started + + Please choose an option: + 1. Return to feature worktree and rebase again + 2. Inspect the branches with: git log --oneline --graph --all + 3. Abort this operation + + What would you like to do? + ``` + + - Wait for user decision and follow their instruction + - **Critical:** Do NOT attempt `git merge` without `--ff-only` flag + +### 5. Delete Feature Worktree + +**Goal:** Clean up the feature worktree directory after successful merge + +1. **Store the feature worktree path:** + - Before switching to main worktree in step 3, store the feature worktree path + - Example: `FEATURE_WORKTREE_PATH=$(pwd)` + +2. **Attempt to remove worktree:** + + ```bash + git worktree remove + ``` + +3. **Handle removal outcomes:** + + **Success (exit code 0):** + - Worktree removed successfully, proceed to step 6 + + **Failure - Untracked files (common):** + - Git will refuse with error like: "fatal: '' contains modified or untracked files, use --force to delete it" + - Check for untracked files: + + ```bash + cd + git status --porcelain | grep '^??' + ``` + + - If untracked files exist, prompt user: + + ```text + ⚠️ Feature worktree contains untracked files: + + + How would you like to proceed? + 1. Create a new commit with these files + 2. Amend the last commit to include these files + 3. Force-delete the worktree (rm -rf) - files will be lost + 4. Abort - leave worktree intact for manual cleanup + + Enter your choice (1-4): + ``` + + - Wait for user input and proceed accordingly: + + **Option 1 - Create new commit:** + + ```bash + cd + git add -A + git commit -m "chore: add remaining untracked files" + # Now need to update main again with this commit + cd + git merge --ff-only + git worktree remove + git branch -d + ``` + + **Option 2 - Amend last commit:** + + ```bash + cd + git add -A + git commit --amend --no-edit + # Now need to update main again with amended commit + cd + # Use reset since history was rewritten + FEATURE_SHA=$(cd && git rev-parse HEAD) + git reset --hard $FEATURE_SHA + git worktree remove + git branch -d + ``` + + **Option 3 - Force delete:** + + ```bash + rm -rf + git worktree prune + ``` + + - Warn user: "⚠️ Untracked files have been permanently deleted." + + **Option 4 - Abort:** + - Leave worktree intact + - Inform user in output that worktree still exists + - User can manually investigate and decide later + + **Failure - Other errors:** + - Display git error message + - Offer force delete with `git worktree remove --force` or manual `rm -rf` + - Ask user for guidance + +4. **Verify worktree removal:** + + ```bash + git worktree list + ``` + + - Ensure feature worktree no longer appears in list + - If it still appears, run `git worktree prune` + +### 6. Delete Feature Branch + +**Goal:** Clean up the feature branch after successful merge + +1. **Delete the local feature branch:** + + ```bash + git branch -d + ``` + +2. **Handle deletion outcomes:** + + **Success:** + - Branch deleted successfully, proceed to step 7 + + **Failure (branch not fully merged):** + - This shouldn't happen since we just fast-forwarded + - If it does, check if there's a remote tracking branch issue + - Offer force deletion only if user confirms: + + ```text + ⚠️ Git reports the branch is not fully merged. + This is unexpected after a successful fast-forward. + + Use `git branch -D ` to force delete? (y/n) + ``` + +### 7. Output Format + +Provide clear summary of the completed operation: + +```text +✅ Successfully merged feature branch '' into main + +Actions completed: + 1. ✅ Rebased onto main + 2. ✅ Fast-forwarded main to + 3. ✅ Removed feature worktree + 4. ✅ Deleted local branch + +Current state: + 📍 Location: + 🌿 Branch: main + 📝 Commits: + +✨ Linear history preserved — no merge commits created! +``` + +**Note:** If worktree removal was skipped (user chose option 4), modify output to say: + +```text +⚠️ Feature worktree was NOT removed (user choice) + 📁 Worktree location: + Manual cleanup: git worktree remove --force +``` + +## Example Usage + +```bash +# Let auto-detect find main worktree +/tools:finish-worktree + +# Specify main worktree path explicitly +/tools:finish-worktree ~/projects/my-project + +# From within a feature worktree +cd ~/projects/my-project-feature +/tools:finish-worktree ../my-project +``` + +## Constraints + +- **NEVER create merge commits** — linear history is paramount +- **NEVER skip commits** during rebase +- **NEVER alter commit messages** without explicit user permission +- **NEVER use `git merge` without `--ff-only` flag** +- **ALWAYS verify working tree is clean** before operations +- **ALWAYS use rebase for history integration** +- **ALWAYS validate fast-forward is possible** before merging + +## Edge Cases + +### 1. Rebase Conflicts + +**Scenario:** Feature branch conflicts with main during rebase + +**Handling:** + +- Stop and list conflicting files +- Provide clear instructions for resolution +- Wait for user to resolve and continue +- User calls `/tools:finish-worktree` again after resolving conflicts +- Command detects rebase in progress and resumes (doesn't restart) +- Never automatically skip or abort rebase + +### 2. Resuming After Rebase Conflicts + +**Scenario:** User calls `/tools:finish-worktree` again after resolving rebase conflicts + +**Handling:** + +- Detect rebase in progress by checking for `.git/rebase-merge` or `.git/rebase-apply` +- Skip fetch and rebase initiation steps +- Jump directly to conflict handling (Step 2.4) +- Check current rebase state: + - If conflicts still exist → show conflict resolution instructions + - If conflicts resolved but not continued → instruct to run `git rebase --continue` + - If rebase completed → proceed to step 3 (Navigate to Main Worktree) +- This allows iterative conflict resolution across multiple command invocations +- Prevents "rebase already in progress" errors + +**Implementation notes:** + +- Use `test -d .git/rebase-merge || test -d .git/rebase-apply` to detect +- Use `git status` to determine current state within rebase +- Never attempt `git rebase main` if rebase already in progress + +### 3. No Main Worktree Found + +**Scenario:** Auto-detection cannot find main worktree + +**Handling:** + +- Prompt user for path +- Validate provided path thoroughly +- Show `git worktree list` output to help user +- Accept either absolute or relative paths + +### 4. Fast-Forward Impossible + +**Scenario:** Main has diverged from feature branch + +**Handling:** + +- Show detailed error message +- Offer three options: re-rebase, inspect history, abort +- Never attempt regular merge +- Explain why fast-forward failed +- Guide user through resolution + +### 5. Working Tree Not Clean + +**Scenario:** Uncommitted changes in current or main worktree + +**Handling:** + +- Detect before any operations begin +- Show clear error message +- Suggest `git stash` or committing changes +- Exit without making any changes + +### 6. Multiple Main Worktrees + +**Scenario:** User has multiple worktrees checked out to main (unusual but possible) + +**Handling:** + +- List all candidates +- Ask user to specify which one to use +- Validate selection before proceeding + +### 7. Feature Branch Doesn't Exist Locally in Main Worktree + +**Scenario:** Feature branch only exists in its worktree, not visible from main worktree + +**Handling:** + +- Attempt to resolve branch reference from feature worktree +- Get commit SHA and use that for fast-forward merge +- If that fails, explain that branch needs to be visible +- Ask user to investigate why branch isn't visible in main worktree + +### 8. Worktree Contains Untracked Files + +**Scenario:** Git refuses to delete worktree because it contains untracked files + +**Handling:** + +- Detect untracked files with `git status --porcelain | grep '^??'` +- List all untracked files to user +- Provide four clear options: + 1. Create new commit with untracked files (then update main) + 2. Amend last commit to include untracked files (then update main) + 3. Force-delete worktree with `rm -rf` (permanent data loss) + 4. Abort and leave worktree for manual inspection +- Wait for explicit user choice +- Execute chosen option carefully +- If option 1 or 2: Must update main branch again since new commits were added, then delete the branch +- If option 3: Warn about permanent deletion before executing +- If option 4: Document worktree location in output for later cleanup +- Never automatically force-delete without user permission + +**Important:** Options 1 and 2 require going back to update the main branch since new commits were created after the initial fast-forward merge, then removing the worktree, then deleting the branch. This maintains the guarantee of linear history. + +## Implementation Notes + +### Git Worktree Primer + +- **Worktree:** Separate working directory for the same repository +- **Main worktree:** Original checkout (typically on main branch) +- **Linked worktree:** Additional checkouts for feature branches + +### Fast-Forward Merge Requirement + +A fast-forward merge is only possible when: + +- Target branch (main) is an ancestor of source branch (feature) +- No divergent commits exist +- History is linear after rebase + +### Why Linear History Matters + +- Easier to understand project evolution +- Simpler to bisect and revert +- Cleaner git log output +- No "merge commit noise" + +### Command Reference + +```bash +# List all worktrees +git worktree list + +# Show current branch +git branch --show-current + +# Rebase onto main +git rebase main + +# Fast-forward only merge +git merge --ff-only + +# Delete merged branch +git branch -d + +# Force delete branch +git branch -D +``` + +## Troubleshooting + +### "fatal: Needed a single revision" + +**Cause:** Branch name is ambiguous or doesn't exist in current context + +**Fix:** Use full commit SHA or ensure branch is visible in current worktree + +### "fatal: Not possible to fast-forward, aborting" + +**Cause:** Main has commits not in feature branch (history diverged) + +**Fix:** Return to feature worktree, pull latest main, rebase again + +### "error: Cannot delete branch (not fully merged)" + +**Cause:** Git detects commits in feature branch not in main + +**Fix:** Verify fast-forward actually succeeded with `git log --oneline --graph` + +## Related Commands + +- `git worktree list` — View all worktrees +- `git worktree remove ` — Remove worktree after merge +- `git rebase --abort` — Cancel rebase if needed +- `git reflog` — Recover from mistakes diff --git a/hooks/hooks.json b/hooks/hooks.json new file mode 100644 index 0000000..585fc38 --- /dev/null +++ b/hooks/hooks.json @@ -0,0 +1,16 @@ +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Write|Edit", + "hooks": [ + { + "type": "command", + "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/eslint-fix.cjs", + "timeout": 30 + } + ] + } + ] + } +} diff --git a/hooks/scripts/eslint-fix.cjs b/hooks/scripts/eslint-fix.cjs new file mode 100755 index 0000000..cae82d8 --- /dev/null +++ b/hooks/scripts/eslint-fix.cjs @@ -0,0 +1,136 @@ +#!/usr/bin/env node +const { execSync } = require('child_process'); +const { readFileSync } = require('fs'); +const { basename, relative } = require('path'); + +/** + * @typedef {Object} HookInput + * @property {string} [cwd] + * @property {string} [file_path] + */ + +/** + * @typedef {Object} ESLintMessage + * @property {number} line + * @property {number} column + * @property {string} message + * @property {number} severity + * @property {string} [ruleId] + */ + +/** + * @typedef {Object} ESLintResult + * @property {ESLintMessage[]} [messages] + */ + +// Read hook input from stdin +/** @type {HookInput} */ +let input; +try { + input = JSON.parse(readFileSync(0, 'utf-8')); +} catch { + // Invalid JSON, skip gracefully + console.log(JSON.stringify({ continue: true })); + process.exit(0); +} + +const { cwd, file_path } = input; + +// Validate required file_path field +if (!file_path) { + console.log(JSON.stringify({ continue: true })); + process.exit(0); +} + +// Only process JS/TS files (including .d.ts declaration files) +const JS_TS_EXTENSIONS = /\.(ts|tsx|js|jsx|cjs|mjs|mts|cts)$/i; +if (!JS_TS_EXTENSIONS.test(file_path)) { + console.log(JSON.stringify({ continue: true })); + process.exit(0); +} + +// Check if eslint is available +try { + execSync('npx eslint --version', { + cwd, + stdio: 'ignore', + }); +} catch { + // ESLint not available, skip silently + console.log(JSON.stringify({ continue: true })); + process.exit(0); +} + +// Run eslint --fix with JSON output +// Use relative path to avoid ESLint base path issues +const relativePath = cwd ? relative(cwd, file_path) : file_path; +/** @type {string} */ +let output; +try { + output = execSync(`npx eslint --fix --format json "${relativePath}"`, { + cwd, + encoding: 'utf-8', + stdio: 'pipe', + }); +} catch (error) { + // ESLint exits with non-zero when errors exist + const stdout = + error && typeof error === 'object' && 'stdout' in error + ? error.stdout + : null; + output = (typeof stdout === 'string' ? stdout : null) || '[]'; +} + +// Parse results and extract remaining errors +/** @type {ESLintResult[]} */ +let results; +try { + const parsed = JSON.parse(output); + results = Array.isArray(parsed) ? parsed : []; +} catch { + // Invalid JSON, assume no errors + results = []; +} + +const errors = + results[0]?.messages?.filter( + (/** @type {ESLintMessage} */ msg) => msg.severity === 2, + ) || []; + +// Format and report errors if any exist +if (errors.length > 0) { + const fileName = basename(file_path); + const MAX_ERRORS_TO_SHOW = 5; + + let errorList; + if (errors.length > MAX_ERRORS_TO_SHOW) { + const shown = errors.slice(0, MAX_ERRORS_TO_SHOW); + errorList = shown + .map( + (/** @type {ESLintMessage} */ e) => + `Line ${e.line}:${e.column}: ${e.message} (${e.ruleId || 'unknown'})`, + ) + .join('\n'); + errorList += `\n...and ${errors.length - MAX_ERRORS_TO_SHOW} more error(s)`; + } else { + errorList = errors + .map( + (/** @type {ESLintMessage} */ e) => + `Line ${e.line}:${e.column}: ${e.message} (${e.ruleId || 'unknown'})`, + ) + .join('\n'); + } + + const message = `ESLint found ${errors.length} error(s) in ${fileName}:\n${errorList}`; + + console.log( + JSON.stringify({ + continue: true, + systemMessage: message, + }), + ); +} else { + console.log(JSON.stringify({ continue: true })); +} + +process.exit(0); diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..f7cc8a8 --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,69 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:boneskull/claude-plugins:plugins/tools", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "53a680f600566f64eb5581844f29419498cdf69f", + "treeHash": "db736367145f5979ce54df00506b1452afa4ceb524adfdb66ff4e695bb75b78e", + "generatedAt": "2025-11-28T10:14:19.561642Z", + "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": "tools", + "description": "Skills and documentation for various CLI, development, and language-specific tools", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "1a22d112b27481def9e8cf7539b81abd7c86c753e400c44e9329f59032fb901f" + }, + { + "path": "hooks/hooks.json", + "sha256": "66be7bf5ca4b6cf8c671265f1fc092cd2212a65a2ae8b4b29b3438aed630fd13" + }, + { + "path": "hooks/scripts/eslint-fix.cjs", + "sha256": "0d2a32f1d771f2181a1fe8d36cee4446abd6c8ca815a8cbf21930ad555c39a43" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "04210789387f1263ac33d89be378ef0f793f30bfaf2818c5054564e21d7dbe45" + }, + { + "path": "commands/apply-template.md", + "sha256": "837114dcfe6cba188c089acf3f088d775b33d5da303a4f50776999a7af70fd3e" + }, + { + "path": "commands/finish-worktree.md", + "sha256": "b4218be16032004fab9852d709b6ace3a584dcab9581b1afe1180b81d835e4f9" + }, + { + "path": "skills/skill-rules.json", + "sha256": "5e8d5b303d83c37c53408491e112a1d510cfd63d2aa8cb6a4c23732ca56931e8" + }, + { + "path": "skills/git-directory-management/SKILL.md", + "sha256": "08c08bc36f03fac8b2be2bf02b3f7e05c35ea2e236617a93723be65fb6587b43" + }, + { + "path": "skills/git-commit-messages/SKILL.md", + "sha256": "f10ba43aa95f73e9624971bffb305f86fa2c8ab34dff3b30f6f90e0965ea0361" + } + ], + "dirSha256": "db736367145f5979ce54df00506b1452afa4ceb524adfdb66ff4e695bb75b78e" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/git-commit-messages/SKILL.md b/skills/git-commit-messages/SKILL.md new file mode 100644 index 0000000..a1e72ed --- /dev/null +++ b/skills/git-commit-messages/SKILL.md @@ -0,0 +1,145 @@ +--- +name: git-commit-messages +description: Format git commit messages correctly, avoiding HEREDOC syntax issues in favor of multiline strings +--- + +# Git Commit Message Formatting + +## When to Use This Skill + +Use this skill when creating git commits with the Bash tool, especially for commits with: + +- Multi-line messages +- Body text or explanations +- Co-authored-by footers +- Claude Code attribution + +## Core Principle + +**Avoid HEREDOC syntax** - Use `-m` with multiline strings instead. + +## Pattern to Follow + +### ✅ DO - Use multiline string with -m + +```bash +git commit -m "feat(github): add github plugin + +This adds comprehensive GitHub workflow support including +commands for PR management and review workflows. + +🤖 Generated with [Claude Code](https://claude.com/claude-code) + +Co-Authored-By: Claude " +``` + +**Key points:** + +- Single `-m` flag with entire message in quotes +- Newlines preserved within the quoted string +- No special syntax or escaping needed +- Works reliably across all environments + +### ❌ DON'T - Use HEREDOC syntax + +```bash +# This FAILS in Claude Code's Bash tool +git commit -m "$(cat <<'EOF' +feat(github): add github plugin + +This adds comprehensive GitHub workflow support. + +🤖 Generated with [Claude Code](https://claude.com/claude-code) + +Co-Authored-By: Claude +EOF +)" +``` + +**Why this fails:** + +- HEREDOC syntax has shell interpretation issues +- Fails in certain execution contexts +- More complex than necessary +- Unreliable across environments + +## Examples + +### Simple commit + +```bash +git commit -m "fix: resolve path configuration issue" +``` + +### Commit with body + +```bash +git commit -m "feat: add progressive disclosure pattern + +Skills now support references/ subdirectory for +detailed documentation that loads on-demand." +``` + +### Commit with footer + +```bash +git commit -m "chore: update dependencies + +🤖 Generated with [Claude Code](https://claude.com/claude-code) + +Co-Authored-By: Claude " +``` + +### Full conventional commit + +```bash +git commit -m "feat(tools): add git commit message skill + +This skill teaches Claude to format git commit messages +using multiline strings instead of HEREDOC syntax, which +fails in certain shell environments. + +The pattern uses a single -m flag with the entire message +in quotes, preserving newlines naturally. + +🤖 Generated with [Claude Code](https://claude.com/claude-code) + +Co-Authored-By: Claude " +``` + +## Why It Matters + +Using `-m` with multiline strings is: + +- **Reliable:** Works consistently in all environments +- **Simple:** No complex shell syntax needed +- **Portable:** Standard git behavior +- **Direct:** Git handles newlines correctly + +HEREDOC syntax causes issues because: + +- Shell interpretation varies by environment +- Execution context in Claude Code differs from terminal +- Quote handling becomes complex +- Unnecessary complexity for the task + +## Quick Reference + +**Template for commits with Claude Code attribution:** + +```bash +git commit -m "[optional scope]: + +[optional body] + +🤖 Generated with [Claude Code](https://claude.com/claude-code) + +Co-Authored-By: Claude " +``` + +**Remember:** + +- One `-m` flag +- Entire message in double quotes +- Newlines work naturally +- No HEREDOC needed diff --git a/skills/git-directory-management/SKILL.md b/skills/git-directory-management/SKILL.md new file mode 100644 index 0000000..576313f --- /dev/null +++ b/skills/git-directory-management/SKILL.md @@ -0,0 +1,209 @@ +--- +name: git-directory-management +description: Manage git-tracked directories correctly - never create .gitkeep files in directories that will immediately contain tracked files +--- + +# Git Directory Management + +## When to Use This Skill + +Use this skill when creating new directories in a git repository, especially when: + +- Setting up project structure +- Creating plugin/package directories +- Organizing code into new folders +- Adding configuration directories + +## Core Principle + +**Never create `.gitkeep` files in directories you're about to populate with tracked files.** + +`.gitkeep` is ONLY for keeping truly empty directories in version control. + +## Pattern to Follow + +### ✅ DO - Add actual files directly + +```bash +# Create directory and add actual file +mkdir -p plugins/new-plugin/skills +# Now add your actual files +# (Write SKILL.md, plugin.json, etc.) +``` + +**Key points:** + +- Git automatically tracks directories when they contain files +- Empty directory + file creation can happen in one step +- No `.gitkeep` needed - the real files track the directory + +### ❌ DON'T - Create .gitkeep then immediately add files + +```bash +# This is wasteful and wrong +mkdir -p plugins/new-plugin/skills +touch plugins/new-plugin/skills/.gitkeep # ❌ Unnecessary! +git add plugins/new-plugin/skills/.gitkeep +git commit -m "Add empty directory" + +# Then immediately add real files +# (Write SKILL.md) +git add plugins/new-plugin/skills/ +git commit -m "Add actual skill" +``` + +**Why this is wrong:** + +- `.gitkeep` serves no purpose if files are coming +- Creates unnecessary commits +- Clutters directory with placeholder file +- Extra file to maintain/remove later + +## When to Use .gitkeep + +`.gitkeep` is appropriate ONLY when: + +1. **The directory must exist but remain empty** +2. **The empty directory is required for the application to function** +3. **No files will be added to the directory immediately** + +**Valid use case example:** + +```bash +# Application requires logs/ directory to exist on startup +mkdir -p logs +touch logs/.gitkeep +git add logs/.gitkeep +git commit -m "chore: add logs directory for runtime output" +``` + +**Why this is valid:** + +- Directory must exist before application runs +- Directory will be populated at runtime (not in version control) +- `.gitkeep` ensures the empty directory is tracked + +## Common Scenarios + +### Scenario: Creating a new plugin structure + +✅ **DO:** + +```bash +mkdir -p plugins/new-plugin/{skills,commands} +# Then immediately create your files: +# Write plugins/new-plugin/.claude-plugin/plugin.json +# Write plugins/new-plugin/skills/skill-name/SKILL.md +# Write plugins/new-plugin/README.md +# Add all files in one commit +git add plugins/new-plugin/ +git commit -m "feat: add new-plugin" +``` + +❌ **DON'T:** + +```bash +mkdir -p plugins/new-plugin/skills +touch plugins/new-plugin/skills/.gitkeep # ❌ Wrong! +# Then add real files later +``` + +### Scenario: Creating empty directories for runtime + +✅ **DO:** + +```bash +mkdir -p tmp/cache +touch tmp/cache/.gitkeep +git add tmp/cache/.gitkeep +git commit -m "chore: add cache directory for runtime" +``` + +**Why this is correct:** Cache directory must exist but contents are not tracked. + +### Scenario: Setting up tool configuration directories + +✅ **DO:** + +```bash +mkdir -p .config/tool +# Immediately add configuration file +# Write .config/tool/config.json +git add .config/tool/config.json +git commit -m "feat: add tool configuration" +``` + +❌ **DON'T:** + +```bash +mkdir -p .config/tool +touch .config/tool/.gitkeep # ❌ Wrong! You're about to add config.json +``` + +## Decision Tree + +```text +Creating a new directory? +│ +├─ Will you add tracked files immediately? +│ └─ YES → No .gitkeep needed, just add the files +│ +└─ Will the directory stay empty in version control? + │ + ├─ YES, and it must exist → Use .gitkeep + │ + └─ NO, files coming later → Wait until files exist, then commit +``` + +## Why It Matters + +**Benefits of not using .gitkeep unnecessarily:** + +- Cleaner repository (fewer placeholder files) +- Fewer commits (one commit with actual content) +- No cleanup needed later (no .gitkeep to remove) +- Clear intent (tracked files show directory purpose) + +**Problems with unnecessary .gitkeep:** + +- Clutters directories with meaningless files +- Creates confusing git history (empty → populated) +- Requires eventual cleanup +- Adds maintenance burden + +## Quick Reference + +**Rule of thumb:** + +- **About to add files?** → No `.gitkeep` +- **Directory stays empty?** → Use `.gitkeep` +- **Not sure yet?** → Wait until files exist + +**Remember:** + +- Git tracks directories through their files +- `.gitkeep` is for truly empty directories +- One commit with actual content beats two commits (empty + populated) +- When in doubt, add the real files first + +## Examples in This Repository + +**Correct usage (runtime directories):** + +- None currently - all directories contain tracked files + +**What NOT to do:** + +```bash +# ❌ DON'T create .gitkeep in plugins/tools/skills/ +# This directory is meant to contain skills, not stay empty +``` + +**Correct approach:** + +```bash +# ✅ Just add your skill directly +# Write plugins/tools/skills/new-skill/SKILL.md +git add plugins/tools/skills/new-skill/ +git commit -m "feat(tools): add new-skill" +``` diff --git a/skills/skill-rules.json b/skills/skill-rules.json new file mode 100644 index 0000000..8f249a8 --- /dev/null +++ b/skills/skill-rules.json @@ -0,0 +1,48 @@ +{ + "description": "Skill activation rules for tools plugin", + "skills": { + "tools@boneskull-plugins:git-commit-messages": { + "type": "guardrail", + "enforcement": "suggest", + "priority": "high", + "description": "Format git commit messages correctly using multiline strings instead of HEREDOC", + "promptTriggers": { + "keywords": [ + "git commit", + "commit message", + "create commit", + "commit changes", + "commit with message" + ], + "intentPatterns": [ + "(create|make|add|write).*commit", + "commit.*message", + "(how to|how do I).*commit" + ] + } + }, + "tools@boneskull-plugins:git-directory-management": { + "type": "guardrail", + "enforcement": "block", + "priority": "critical", + "description": "Prevent unnecessary .gitkeep files in directories that will contain tracked files", + "promptTriggers": { + "keywords": [ + ".gitkeep", + "git keep", + "track empty directory", + "keep directory in git", + "empty directory git" + ], + "intentPatterns": [ + "create.*\\.gitkeep", + "add.*\\.gitkeep", + "(track|keep).*empty.*dir", + "git.*track.*directory" + ] + }, + "blockMessage": "⚠️ BLOCKED - .gitkeep Anti-Pattern Detected\n\n📋 REQUIRED ACTION:\n1. Use Skill tool: 'tools@boneskull-plugins:git-directory-management'\n2. Review when .gitkeep is actually needed (rarely!)\n3. Add actual files instead of .gitkeep\n\nReason: .gitkeep is ONLY for truly empty directories.\nIf you're about to add files, just add them directly.\n\n💡 TIP: Git tracks directories automatically when they contain files" + } + }, + "version": "1.0" +}