commit 8c063d258eade8c5a5a4f1f491c4e6dc21e6fb6d Author: Zhongwei Li Date: Sun Nov 30 08:44:15 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..d442e20 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "handbook-dotnet", + "description": ".NET development tools including automatic CSharpier formatting for C# files", + "version": "1.12.0", + "author": { + "name": "nikiforovall", + "url": "https://github.com/nikiforovall" + }, + "hooks": [ + "./hooks" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ccd91ac --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# handbook-dotnet + +.NET development tools including automatic CSharpier formatting for C# files diff --git a/hooks/format-csharp.sh b/hooks/format-csharp.sh new file mode 100644 index 0000000..4618ee3 --- /dev/null +++ b/hooks/format-csharp.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# shellcheck shell=bash +set -euo pipefail + +# Check if hook is disabled via environment variable +if [ "${CC_HANDBOOK_DOTNET_DISABLE_HOOKS:-false}" = "true" ]; then + exit 0 +fi + +# Read JSON input from stdin +input=$(cat) + +# Extract file path from tool_input +file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty') + +# Exit silently if no file path +if [ -z "$file_path" ]; then + exit 0 +fi + +# Only process C# files (.cs, .CS, .csx, .CSX) +if [[ ! "$file_path" =~ \.(cs|CS|csx|CSX)$ ]]; then + exit 0 +fi + +# Check if file exists +if [ ! -f "$file_path" ]; then + exit 0 +fi + +# Determine which CSharpier command to use +# Try dotnet csharpier first (local or global dotnet tool) +if dotnet csharpier --version &>/dev/null; then + FORMATTER_CMD="dotnet csharpier" +# Fall back to csharpier executable (global standalone) +elif command -v csharpier &>/dev/null; then + FORMATTER_CMD="csharpier" +else + echo "Error: CSharpier not found. Install with: dotnet tool install -g csharpier" >&2 + exit 1 +fi + +# Run CSharpier with specified options +# --skip-validation: Skip validation for better performance +# --compilation-errors-as-warnings: Don't block on compilation errors +if ! $FORMATTER_CMD format "$file_path" --skip-validation --compilation-errors-as-warnings 2>&1; then + echo "Warning: CSharpier formatting failed for $file_path" >&2 + exit 1 # Non-zero exit (not 2) - warns user but doesn't block Claude +fi + +exit 0 diff --git a/hooks/hooks.json b/hooks/hooks.json new file mode 100644 index 0000000..e20613a --- /dev/null +++ b/hooks/hooks.json @@ -0,0 +1,17 @@ +{ + "description": "Automatic CSharpier formatting for C# files", + "hooks": { + "PostToolUse": [ + { + "matcher": "Write|Edit", + "hooks": [ + { + "type": "command", + "command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/format-csharp.sh\"", + "timeout": 30 + } + ] + } + ] + } +} diff --git a/hooks/test-format-hook.sh b/hooks/test-format-hook.sh new file mode 100644 index 0000000..0ca800e --- /dev/null +++ b/hooks/test-format-hook.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash +# shellcheck shell=bash +# Test script for CSharpier formatter hook + +set -euo pipefail + +# Get script directory and setup +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +HOOK_SCRIPT="$SCRIPT_DIR/format-csharp.sh" +TEST_DIR=$(mktemp -d) +trap 'rm -rf "$TEST_DIR"' EXIT + +# Test counters +PASSED=0 +FAILED=0 +SKIPPED=0 + +# Convert paths for Windows (MINGW/Git Bash) +to_path() { + if command -v cygpath &>/dev/null; then + cygpath -w "$1" | sed 's/\\/\\\\/g' + else + echo "$1" + fi +} + +# Run a test +test_hook() { + local name="$1" + local json="$2" + local env="${3:-}" + + echo -n " $name ... " + + local exit_code=0 + if [ -n "$env" ]; then + echo "$json" | $env bash "$HOOK_SCRIPT" &>/dev/null || exit_code=$? + else + echo "$json" | bash "$HOOK_SCRIPT" &>/dev/null || exit_code=$? + fi + + if [ "$exit_code" -eq 0 ]; then + echo "✓" + PASSED=$((PASSED + 1)) + else + echo "✗ (exit code: $exit_code)" + FAILED=$((FAILED + 1)) + fi +} + +echo "==============================" +echo "CSharpier Hook Test Suite" +echo "==============================" +echo "" + +# File Type Filtering Tests +echo "File Type Filtering:" +echo "namespace Test{class X{}}" > "$TEST_DIR/test.cs" +test_hook "Processes .cs files" "{\"tool_input\":{\"file_path\":\"$(to_path "$TEST_DIR/test.cs")\"}}" + +echo "class Test{}" > "$TEST_DIR/test.CS" +test_hook "Processes .CS files" "{\"tool_input\":{\"file_path\":\"$(to_path "$TEST_DIR/test.CS")\"}}" + +echo "Console.WriteLine(\"test\");" > "$TEST_DIR/test.csx" +test_hook "Processes .csx files" "{\"tool_input\":{\"file_path\":\"$(to_path "$TEST_DIR/test.csx")\"}}" + +echo "text" > "$TEST_DIR/test.txt" +test_hook "Ignores .txt files" "{\"tool_input\":{\"file_path\":\"$(to_path "$TEST_DIR/test.txt")\"}}" + +echo "text" > "$TEST_DIR/test.cs.bak" +test_hook "Ignores .cs.bak files" "{\"tool_input\":{\"file_path\":\"$(to_path "$TEST_DIR/test.cs.bak")\"}}" + +echo "" +echo "Input Validation:" +test_hook "Handles missing file_path" "{\"tool_input\":{}}" +test_hook "Handles empty file_path" "{\"tool_input\":{\"file_path\":\"\"}}" +test_hook "Handles nonexistent file" "{\"tool_input\":{\"file_path\":\"$(to_path "$TEST_DIR/missing.cs")\"}}" + +echo "" +echo "Environment Variable:" + +# Test that env var actually prevents formatting +cat > "$TEST_DIR/env-test.cs" << 'EOF' +class Example +{ +} +EOF +BEFORE=$(cat "$TEST_DIR/env-test.cs") +ENV_PATH=$(to_path "$TEST_DIR/env-test.cs") + +echo -n " Respects DISABLE_HOOKS=true ... " +echo "{\"tool_input\":{\"file_path\":\"$ENV_PATH\"}}" | CC_HANDBOOK_DOTNET_DISABLE_HOOKS=true bash "$HOOK_SCRIPT" &>/dev/null || true +AFTER=$(cat "$TEST_DIR/env-test.cs") +if [ "$BEFORE" = "$AFTER" ]; then + echo "✓" + PASSED=$((PASSED + 1)) +else + echo "✗ (file was formatted despite env var)" + FAILED=$((FAILED + 1)) +fi + +# Verify formatting works without env var +cat > "$TEST_DIR/env-test2.cs" << 'EOF' +class Example +{ +} +EOF +BEFORE=$(cat "$TEST_DIR/env-test2.cs") +ENV_PATH2=$(to_path "$TEST_DIR/env-test2.cs") + +echo -n " Works by default (no env var) ... " +echo "{\"tool_input\":{\"file_path\":\"$ENV_PATH2\"}}" | bash "$HOOK_SCRIPT" &>/dev/null || true +AFTER=$(cat "$TEST_DIR/env-test2.cs") +if [ "$BEFORE" != "$AFTER" ]; then + echo "✓" + PASSED=$((PASSED + 1)) +else + echo "✗ (file was not formatted)" + FAILED=$((FAILED + 1)) +fi + +echo "" +echo "Code Formatting:" + +# Check if CSharpier is available +if ! command -v csharpier &>/dev/null && ! dotnet csharpier --version &>/dev/null 2>&1; then + echo " Formats C# code ... ⊘ (CSharpier not installed)" + SKIPPED=$((SKIPPED + 1)) +else + # Create badly formatted code + cat > "$TEST_DIR/format-test.cs" << 'EOF' +class Example +{ +} +EOF + BEFORE=$(cat "$TEST_DIR/format-test.cs") + + # Run formatter + FMT_PATH=$(to_path "$TEST_DIR/format-test.cs") + echo "{\"tool_input\":{\"file_path\":\"$FMT_PATH\"}}" | bash "$HOOK_SCRIPT" &>/dev/null || true + + AFTER=$(cat "$TEST_DIR/format-test.cs") + + echo -n " Formats C# code ... " + if [ "$BEFORE" != "$AFTER" ]; then + echo "✓" + PASSED=$((PASSED + 1)) + else + echo "✗ (no formatting occurred)" + FAILED=$((FAILED + 1)) + fi +fi + +# Results +echo "" +echo "==============================" +TOTAL=$((PASSED + FAILED + SKIPPED)) +echo "Results: $PASSED/$TOTAL passed" +[ "$SKIPPED" -gt 0 ] && echo " $SKIPPED skipped" +echo "==============================" + +[ "$FAILED" -eq 0 ] diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..da7fcbd --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,53 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:NikiforovAll/claude-code-rules:plugins/handbook-dotnet", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "033b5e46483609dd68d996c73efea0e2b4c42a2b", + "treeHash": "d8373a4b2761bd13c70ebe7f0924157e1713e521f1d5718185e5852ae7c5e0f4", + "generatedAt": "2025-11-28T10:12:15.433975Z", + "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": "handbook-dotnet", + "description": ".NET development tools including automatic CSharpier formatting for C# files", + "version": "1.12.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "4895a29f5670e4b603b954f63798cfb27a45c4ee2cda03c57932bb00c227c091" + }, + { + "path": "hooks/test-format-hook.sh", + "sha256": "c7f9b961f853224cd8a49a657841ea266dc1857eb2ddc0ae46d998c0a3db2182" + }, + { + "path": "hooks/hooks.json", + "sha256": "9cc521cb96780bc6c90e5cd01250b88016591c8f8bd9f38eb29a341189aa6d4c" + }, + { + "path": "hooks/format-csharp.sh", + "sha256": "592dd618170b925257f42215953807c9bda5fb8c6486a9bb847804905676c2f6" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "8869d80dc0ea9ae931df0200e1d7ee10f6171e54ceb67ae65da2cdae14384a5a" + } + ], + "dirSha256": "d8373a4b2761bd13c70ebe7f0924157e1713e521f1d5718185e5852ae7c5e0f4" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file