Claude Skill

refactoring-csharp

Rename and refactor C# symbols in a .NET solution or multi-solution monorepo with a one-shot Roslyn CLI. Use when the user asks to rename a symbol, preview impact, update references across a solution, or refactor shared projects across several solutions.

LLM Mart · 0 points · 3 views 2 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download codealive-ai-ai-driven-development-skills_refactoring-csharp-68a302a.zip · 34 KB
Part of codealive-ai/ai-driven-development — 21 skills

Install

skills CLI npx skills add https://github.com/CodeAlive-AI/ai-driven-development/tree/main/skills/refactoring-csharp
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install codealive-ai-ai-driven-development@llmmart
Git git clone https://github.com/CodeAlive-AI/ai-driven-development.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole codealive-ai/ai-driven-development collection as a plugin from our marketplace. Git is the plain clone.

README

refactoring-csharp

Inspired by ReSharper. A Roslyn-based refactorer for C# solutions and multi-solution monorepos, packaged as an agent skill with bundled .NET 10 source and optional prebuilt CLI binaries.

Contents

Quick start

macOS / Linux:

curl -fsSL https://github.com/CodeAlive-AI/ai-driven-development/releases/download/refactoring-csharp-v0.1.0/install-refactoring-csharp.sh | bash

Windows PowerShell:

irm https://github.com/CodeAlive-AI/ai-driven-development/releases/download/refactoring-csharp-v0.1.0/install-refactoring-csharp.ps1 | iex

Default install targets Codex, Claude Code, and OpenCode. Install to a specific agent:

curl -fsSL https://github.com/CodeAlive-AI/ai-driven-development/releases/download/refactoring-csharp-v0.1.0/install-refactoring-csharp.sh | bash -s -- --agent cursor
iex "& { $(irm https://github.com/CodeAlive-AI/ai-driven-development/releases/download/refactoring-csharp-v0.1.0/install-refactoring-csharp.ps1) } -Agent cursor"

Install to all supported global agent skill directories:

curl -fsSL https://github.com/CodeAlive-AI/ai-driven-development/releases/download/refactoring-csharp-v0.1.0/install-refactoring-csharp.sh | bash -s -- --all-agents
iex "& { $(irm https://github.com/CodeAlive-AI/ai-driven-development/releases/download/refactoring-csharp-v0.1.0/install-refactoring-csharp.ps1) } -AllAgents"

Pin a release:

REFACTORING_CSHARP_VERSION=refactoring-csharp-v0.1.0 \
  curl -fsSL https://github.com/CodeAlive-AI/ai-driven-development/releases/download/refactoring-csharp-v0.1.0/install-refactoring-csharp.sh | bash
$env:REFACTORING_CSHARP_VERSION = "refactoring-csharp-v0.1.0"
irm https://github.com/CodeAlive-AI/ai-driven-development/releases/download/refactoring-csharp-v0.1.0/install-refactoring-csharp.ps1 | iex

What it does

  • Renames C# symbols across a whole solution using Roslyn.
  • Supports .sln and .slnx.
  • Supports multi-solution monorepos by accepting a directory, merging discovered solutions/projects into a temporary aggregate .slnx, refactoring through Roslyn, and deleting the temporary solution afterward.
  • Resolves targets from file_path, 1-based line_number, and required old_name.
  • Applies by default; dry-run is opt-in.
  • Renames comments by default.
  • Safely moves matching type files by default so git can detect a rename.
  • Uses the target solution's normal MSBuild/Roslyn cache instead of creating a second project cache.
  • Ships source with the skill so agents can build, inspect, patch, and test it locally.

Supported agents

The installer follows the global skill directories from the Skills CLI ecosystem and supports:

adal, amp, antigravity, augment, claude-code, cline, codebuddy, codex, command-code, continue, crush, cursor, droid, gemini-cli, github-copilot, goose, iflow-cli, junie, kilo, kimi-cli, kiro-cli, kode, mcpjam, mistral-vibe, mux, neovate, openclaw, opencode, openhands, pi, pochi, qoder, qwen-code, replit, roo, trae, trae-cn, windsurf, and zencoder.

Useful flags:

  • --agent <id> / -Agent <id> installs to one agent. Repeatable in bash.
  • --all-agents / -AllAgents installs to every supported global skill directory.
  • --detected / -Detected installs only to agents whose global config directory exists.
  • --no-binary / -NoBinary installs only the skill source.

Use directly

Preferred, if installed from release with a prebuilt binary:

~/.codex/skills/refactoring-csharp/bin/csharp-refactor rename-symbol \
  /repo/src/App.slnx \
  /repo/src/Foo.cs \
  42 \
  OldName \
  NewName

For multi-solution monorepos, pass the repository directory instead of a solution file:

~/.codex/skills/refactoring-csharp/bin/csharp-refactor rename-symbol \
  /repo \
  /repo/src/Foo.cs \
  42 \
  OldName \
  NewName

Source fallback:

dotnet run --project ~/.codex/skills/refactoring-csharp/src/CSharpRefactoring.Cli -- \
  rename-symbol /repo/src/App.slnx /repo/src/Foo.cs 42 OldName NewName

line_number is 1-based and can be copied directly from rg -n.

Contract

rename-symbol <sln|slnx|directory> <file> <line> <oldName> <newName> [dryRun=false|true]

Defaults:

  • dryRun=false
  • rename_file=true
  • rename_in_comments=true
  • rename_in_strings=false
  • rename_overloads=false

Build and test

cd skills/refactoring-csharp
dotnet build src/CSharpRefactoring.slnx
dotnet test src/CSharpRefactoring.slnx

Release

cd skills/refactoring-csharp
./release.sh refactoring-csharp-v0.1.0

The release script creates:

  • dist/refactoring-csharp-skill.tar.gz
  • dist/csharp-refactor-darwin-arm64.tar.gz
  • dist/csharp-refactor-darwin-x64.tar.gz
  • dist/csharp-refactor-linux-arm64.tar.gz
  • dist/csharp-refactor-linux-x64.tar.gz
  • dist/csharp-refactor-win-x64.zip
  • dist/SHA256SUMS

License

MIT.

Skill manifest

Refactoring C# Symbols

This skill ships a Roslyn-based C# rename CLI in src/. It is intentionally one-shot and stateless: one call resolves the target, validates the request, and returns either a preview or an applied rename. There is no public prepare step.

Contents

Canonical CLI

If the skill was installed by install.sh, prefer the prebuilt CLI:

bin/csharp-refactor rename-symbol <sln|slnx|directory> <file> <line> <oldName> <newName> [dryRun=false|true]

Otherwise run the bundled source CLI from the skill directory:

dotnet run --project src/CSharpRefactoring.Cli -- rename-symbol <sln|slnx|directory> <file> <line> <oldName> <newName> [dryRun=false|true]

When invoked from outside the skill directory, use an absolute project path:

/<skill-dir>/bin/csharp-refactor rename-symbol <sln|slnx|directory> <file> <line> <oldName> <newName> [dryRun=false|true]
# or, if no prebuilt binary is installed:
dotnet run --project /path/to/refactoring-csharp/src/CSharpRefactoring.Cli -- rename-symbol <sln|slnx|directory> <file> <line> <oldName> <newName> [dryRun=false|true]

The tool project may create normal bin/ and obj/ directories under the skill. That is expected and useful: it lets repeated runs reuse the .NET build cache instead of rebuilding the CLI from scratch.

The target solution is not redirected to a temporary build output. The CLI opens the solution from the solution directory and lets Roslyn/MSBuild use the target project's normal build cache (obj/, bin/, or the repository's configured artifacts layout). This is intentional: it avoids creating a second cache tree for the project being refactored and lets repeated renames benefit from the project's existing MSBuild/Roslyn design-time build cache.

Contract

Field Required Default Notes
solution_path yes - Absolute path to the .sln/.slnx file, or a repository directory for monorepo-wide refactoring.
file_path yes - Absolute path to a file inside the solution.
line_number yes - 1-based line number. Use the value reported by rg -n.
old_name yes - Exact current identifier on that line. This is the anchor.
new_name yes - Must be a valid C# identifier.
dry_run no false Apply changes by default. Preview only when explicitly set to true.
rename_overloads no false Keep overloads unchanged by default.
rename_in_strings no false String literals stay untouched by default.
rename_in_comments no true Comments are renamed by default.
rename_file no true Safe file move for supported named types. Never recreate the file as delete+add.

How To Use It

  1. Use rg -n to locate the symbol and copy the 1-based line number directly.
  2. Call rename-symbol once without the dryRun argument for normal rename requests. This applies the rename.
  3. Use dryRun=true only when the user explicitly asks for preview, when the target is ambiguous, or when the rename is unusually broad/risky and applying immediately would be irresponsible. The CLI also accepts true, --dry-run, dryRun=false, false, and --no-dry-run; invalid values fail fast and must never silently apply.
  4. Do not run a dry run just because the tool supports it. The tool loads the solution on every call, so preview+apply doubles the cost on large projects.
  5. Summarize the result by reporting the original name, new name, changed document count, total text changes, changed files, and any file move.

Monorepo Directory Mode

Use directory mode only when a repository contains multiple .sln/.slnx files or shared projects that are not all present in one normal solution. For ordinary repositories, pass the specific solution file because it is faster and avoids scanning unrelated projects.

When solution_path is a directory:

  • Pass the repository or workspace root as solution_path.
  • Keep file_path as the absolute path to the target source file.
  • The tool recursively discovers .sln, .slnx, and supported project files under the directory.
  • It merges them into one temporary .slnx, opens that aggregate solution, performs the rename, and deletes the temporary solution directory before returning.
  • The temporary aggregate solution lives outside the target repository; source files and normal project build caches remain in their original locations.

Important Behavioral Rules

  • The tool is stateless. It loads the solution on every call.
  • If solution_path is a directory, the tool scans it recursively, merges discovered .sln, .slnx, and project files into one temporary .slnx, opens that solution, runs the rename, and deletes the temporary solution directory before returning.
  • A preview does not reserve state. If the workspace changes between preview and apply, rerun the preview.
  • Prefer one apply call over preview+apply when the user already asked to perform the rename.
  • For a .sln/.slnx input, the tool runs Roslyn from the target solution directory. For a directory input, it runs Roslyn from that directory while opening the temporary aggregate solution. In both modes, project files stay in place and use the target project's normal MSBuild outputs.
  • Do not invent a session or hidden prepare state.
  • Do not ask for a column number. The tool resolves from file_path, line_number, and old_name.
  • old_name is mandatory because it disambiguates the target when a line contains more than one renameable identifier.
  • The bundled source requires .NET 10 and restores NuGet packages on first run.
  • Let the tool keep its own normal bin/ and obj/ cache unless the user explicitly asks for a clean/no-cache run.
  • If the tool returns a preview, say preview. If it returns applied changes, say applied.
  • Keep responses concise and action-oriented. Tell the user what changed and whether a file move happened.

Supported Targets

Treat these as supported rename targets when the Roslyn symbol is source-backed and CanBeReferencedByName:

  • NamedType
  • Method
  • Property
  • Field
  • Event
  • Parameter
  • Local
  • TypeParameter
  • Namespace

Do not rename constructors, destructors, static constructors, or indexers.

File Rename Nuance

rename_file=true is a convenience default, but it only produces a real safe move when the symbol is a single-declaration named type and the file stem matches the current type name.

If the tool does not return file_move_from_path and file_move_to_path, the symbol rename is still valid, but the file itself was not moved. Do not claim a file rename happened unless the tool reports it.

This is intentionally conservative so git sees a tracked rename instead of a delete+add pair.

Error Handling

Use the tool's error codes as actionable guidance:

Error code Meaning What to do
invalid_solution_path Solution path is missing, does not exist, or is neither .sln/.slnx nor a directory. Ask for a real solution path or repository directory.
invalid_file_path File path is missing or not present on disk. Ask for the correct file path.
file_not_in_solution The file is not part of the loaded solution. Ask for the correct file or solution.
invalid_line_number Line number is outside file bounds or not 1-based. Ask for the correct line.
invalid_old_name old_name was empty or whitespace. Ask for the exact current name.
old_name_not_found_on_line No renameable symbol with that name exists on the line. Ask for a better line or file.
ambiguous_old_name_on_line More than one renameable symbol matches that name on the line. Narrow the target or use a different line.
unsupported_symbol_kind Roslyn found a symbol, but this kind is not renameable here. Move to a supported symbol kind.
symbol_not_in_source The symbol is not declared in source. Pick a source-backed target.
invalid_new_name new_name is not a valid C# identifier. Propose a valid identifier.
same_name New name equals the current name. Ask for a different name.
no_changes Roslyn produced no text edits. Re-check the target or the new name.
apply_failed Workspace apply failed. Treat as a runtime failure and retry only if the state is unchanged.
operation_timeout The rename timed out. Retry with a larger timeout or a narrower target.

Success Criteria

A rename workflow is complete when:

  • The target was resolved from line_number + old_name.
  • The user approved the rename, or explicitly requested a dry run only.
  • The tool returned changed documents, total text changes, and any file move details.
  • The final answer makes the applied scope clear enough for the user to trust the change.

Recommended Output Style

  • For previews, say what would change and that nothing was applied.
  • For applied changes, say what changed and whether the file was moved.
  • If the file move fields are present, mention them explicitly.
  • If the tool returned an error code, echo the code and the human-readable reason.
Files (ai-driven-development)
  • src
    • CSharpRefactoring.Cli
      • sample
        • SampleTarget
          • Program.cs 451 B · in bundle
          • SampleRenameDemo.sln 973 B · in bundle
          • SampleRenameDemo.slnx 64 B · in bundle
          • SampleTarget.csproj 161 B · in bundle
      • CSharpRefactoring.Cli.csproj 403 B · in bundle
      • Program.cs 4.2 KB · in bundle
    • CSharpRefactoring.Core
      • DotNetSolutionsMerger
        • SolutionMerger.cs 31.8 KB · in bundle
      • CSharpRefactoring.Core.csproj 587 B · in bundle
      • CSharpSymbolRenamer.cs 42.5 KB · in bundle
    • CSharpRefactoring.Core.Tests
      • CSharpRefactoring.Core.Tests.csproj 1 KB · in bundle
      • CSharpSymbolRenamerTests.cs 14.7 KB · in bundle
    • TestData
      • SampleRenameDemo
        • Program.cs 451 B · in bundle
        • SampleRenameDemo.csproj 161 B · in bundle
        • SampleRenameDemo.sln 973 B · in bundle
        • SampleRenameDemo.slnx 64 B · in bundle
        • SampleTarget.csproj 161 B · in bundle
    • CSharpRefactoring.slnx 255 B · in bundle
  • .gitignore 44 B · in bundle
  • install.ps1 11.1 KB · in bundle
  • install.sh 10.9 KB
    #!/usr/bin/env bash
    set -euo pipefail
    
    REPO="CodeAlive-AI/ai-driven-development"
    SKILL_NAME="refactoring-csharp"
    SKILL_ASSET="refactoring-csharp-skill.tar.gz"
    
    with_binary=1
    source_dir=""
    selected_agents=()
    all_agents=0
    detected_agents=0
    
    AGENT_IDS=(
      adal amp antigravity augment claude-code cline codebuddy codex command-code
      continue crush cursor droid gemini-cli github-copilot goose iflow-cli junie
      kilo kimi-cli kiro-cli kode mcpjam mistral-vibe mux neovate openclaw opencode
      openhands pi pochi qoder qwen-code replit roo trae trae-cn windsurf zencoder
    )
    
    usage() {
      cat <<'EOF'
    Usage:
      install.sh [--agent ID ...] [--all-agents|--detected] [--no-binary] [--source PATH]
    
    Defaults:
      Installs to Codex, Claude Code, and OpenCode global skill directories:
        ~/.codex/skills/refactoring-csharp
        ~/.claude/skills/refactoring-csharp
        ~/.config/opencode/skills/refactoring-csharp
    
    Agent selection:
      --agent codex          Install to one agent. Repeat for multiple agents.
      --codex               Alias for --agent codex.
      --claude              Alias for --agent claude-code.
      --all                 Alias for --all-agents.
      --all-agents          Install to all supported global agent skill directories.
      --detected            Install to supported agents whose global config dir exists.
    
    Environment:
      REFACTORING_CSHARP_VERSION=refactoring-csharp-vX.Y.Z
    
    Supported agent IDs:
      adal amp antigravity augment claude-code cline codebuddy codex command-code
      continue crush cursor droid gemini-cli github-copilot goose iflow-cli junie
      kilo kimi-cli kiro-cli kode mcpjam mistral-vibe mux neovate openclaw opencode
      openhands pi pochi qoder qwen-code replit roo trae trae-cn windsurf zencoder
    EOF
    }
    
    agent_skill_dir() {
      case "$1" in
        adal) echo "$HOME/.adal/skills/$SKILL_NAME" ;;
        amp) echo "$HOME/.config/agents/skills/$SKILL_NAME" ;;
        antigravity) echo "$HOME/.gemini/antigravity/skills/$SKILL_NAME" ;;
        augment) echo "$HOME/.augment/skills/$SKILL_NAME" ;;
        claude-code|claude) echo "$HOME/.claude/skills/$SKILL_NAME" ;;
        cline) echo "$HOME/.cline/skills/$SKILL_NAME" ;;
        codebuddy) echo "$HOME/.codebuddy/skills/$SKILL_NAME" ;;
        codex) echo "$HOME/.codex/skills/$SKILL_NAME" ;;
        command-code) echo "$HOME/.commandcode/skills/$SKILL_NAME" ;;
        continue) echo "$HOME/.continue/skills/$SKILL_NAME" ;;
        crush) echo "$HOME/.config/crush/skills/$SKILL_NAME" ;;
        cursor) echo "$HOME/.cursor/skills/$SKILL_NAME" ;;
        droid) echo "$HOME/.factory/skills/$SKILL_NAME" ;;
        gemini-cli|gemini) echo "$HOME/.gemini/skills/$SKILL_NAME" ;;
        github-copilot|copilot) echo "$HOME/.copilot/skills/$SKILL_NAME" ;;
        goose) echo "$HOME/.config/goose/skills/$SKILL_NAME" ;;
        iflow-cli|iflow) echo "$HOME/.iflow/skills/$SKILL_NAME" ;;
        junie) echo "$HOME/.junie/skills/$SKILL_NAME" ;;
        kilo) echo "$HOME/.kilocode/skills/$SKILL_NAME" ;;
        kimi-cli|kimi) echo "$HOME/.config/agents/skills/$SKILL_NAME" ;;
        kiro-cli|kiro) echo "$HOME/.kiro/skills/$SKILL_NAME" ;;
        kode) echo "$HOME/.kode/skills/$SKILL_NAME" ;;
        mcpjam) echo "$HOME/.mcpjam/skills/$SKILL_NAME" ;;
        mistral-vibe) echo "$HOME/.vibe/skills/$SKILL_NAME" ;;
        mux) echo "$HOME/.mux/skills/$SKILL_NAME" ;;
        neovate) echo "$HOME/.neovate/skills/$SKILL_NAME" ;;
        openclaw) echo "$HOME/.openclaw/skills/$SKILL_NAME" ;;
        opencode) echo "$HOME/.config/opencode/skills/$SKILL_NAME" ;;
        openhands) echo "$HOME/.openhands/skills/$SKILL_NAME" ;;
        pi) echo "$HOME/.pi/agent/skills/$SKILL_NAME" ;;
        pochi) echo "$HOME/.pochi/skills/$SKILL_NAME" ;;
        qoder) echo "$HOME/.qoder/skills/$SKILL_NAME" ;;
        qwen-code|qwen) echo "$HOME/.qwen/skills/$SKILL_NAME" ;;
        replit) echo "$HOME/.config/agents/skills/$SKILL_NAME" ;;
        roo) echo "$HOME/.roo/skills/$SKILL_NAME" ;;
        trae) echo "$HOME/.trae/skills/$SKILL_NAME" ;;
        trae-cn) echo "$HOME/.trae-cn/skills/$SKILL_NAME" ;;
        windsurf) echo "$HOME/.codeium/windsurf/skills/$SKILL_NAME" ;;
        zencoder) echo "$HOME/.zencoder/skills/$SKILL_NAME" ;;
        *) return 1 ;;
      esac
    }
    
    agent_config_dir() {
      case "$1" in
        adal) echo "$HOME/.adal" ;;
        amp|kimi-cli|replit) echo "$HOME/.config/agents" ;;
        antigravity) echo "$HOME/.gemini/antigravity" ;;
        augment) echo "$HOME/.augment" ;;
        claude-code) echo "$HOME/.claude" ;;
        cline) echo "$HOME/.cline" ;;
        codebuddy) echo "$HOME/.codebuddy" ;;
        codex) echo "$HOME/.codex" ;;
        command-code) echo "$HOME/.commandcode" ;;
        continue) echo "$HOME/.continue" ;;
        crush) echo "$HOME/.config/crush" ;;
        cursor) echo "$HOME/.cursor" ;;
        droid) echo "$HOME/.factory" ;;
        gemini-cli) echo "$HOME/.gemini" ;;
        github-copilot) echo "$HOME/.copilot" ;;
        goose) echo "$HOME/.config/goose" ;;
        iflow-cli) echo "$HOME/.iflow" ;;
        junie) echo "$HOME/.junie" ;;
        kilo) echo "$HOME/.kilocode" ;;
        kiro-cli) echo "$HOME/.kiro" ;;
        kode) echo "$HOME/.kode" ;;
        mcpjam) echo "$HOME/.mcpjam" ;;
        mistral-vibe) echo "$HOME/.vibe" ;;
        mux) echo "$HOME/.mux" ;;
        neovate) echo "$HOME/.neovate" ;;
        openclaw) echo "$HOME/.openclaw" ;;
        opencode) echo "$HOME/.config/opencode" ;;
        openhands) echo "$HOME/.openhands" ;;
        pi) echo "$HOME/.pi" ;;
        pochi) echo "$HOME/.pochi" ;;
        qoder) echo "$HOME/.qoder" ;;
        qwen-code) echo "$HOME/.qwen" ;;
        roo) echo "$HOME/.roo" ;;
        trae) echo "$HOME/.trae" ;;
        trae-cn) echo "$HOME/.trae-cn" ;;
        windsurf) echo "$HOME/.codeium/windsurf" ;;
        zencoder) echo "$HOME/.zencoder" ;;
        *) return 1 ;;
      esac
    }
    
    normalize_agent_id() {
      case "$1" in
        claude) echo "claude-code" ;;
        gemini) echo "gemini-cli" ;;
        copilot) echo "github-copilot" ;;
        iflow) echo "iflow-cli" ;;
        kimi) echo "kimi-cli" ;;
        kiro) echo "kiro-cli" ;;
        qwen) echo "qwen-code" ;;
        *) echo "$1" ;;
      esac
    }
    
    add_agent() {
      local id
      id="$(normalize_agent_id "$1")"
      if ! agent_skill_dir "$id" >/dev/null; then
        echo "error: unsupported agent id: $1" >&2
        exit 2
      fi
      selected_agents+=("$id")
    }
    
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --agent|-a)
          shift
          if [[ $# -eq 0 || -z "${1:-}" ]]; then
            echo "error: --agent requires an ID" >&2
            exit 2
          fi
          add_agent "$1"
          ;;
        --agent=*) add_agent "${1#--agent=}" ;;
        --codex) add_agent codex ;;
        --claude) add_agent claude-code ;;
        --all|--all-agents) all_agents=1 ;;
        --detected) detected_agents=1 ;;
        --no-binary) with_binary=0 ;;
        --source)
          shift
          if [[ $# -eq 0 || -z "${1:-}" ]]; then
            echo "error: --source requires a path" >&2
            exit 2
          fi
          source_dir="$1"
          ;;
        --source=*) source_dir="${1#--source=}" ;;
        -h|--help) usage; exit 0 ;;
        *) echo "error: unknown arg: $1" >&2; usage; exit 2 ;;
      esac
      shift
    done
    
    require() {
      command -v "$1" >/dev/null 2>&1 || {
        echo "error: $1 is required" >&2
        exit 1
      }
    }
    
    resolve_tag() {
      if [[ -n "${REFACTORING_CSHARP_VERSION:-}" ]]; then
        echo "$REFACTORING_CSHARP_VERSION"
        return
      fi
    
      curl -fsSL "https://api.github.com/repos/${REPO}/releases?per_page=50" \
        | grep -oE '"tag_name":[[:space:]]*"refactoring-csharp-v[^"]*"' \
        | head -1 \
        | sed -E 's/.*"(refactoring-csharp-v[^"]*)"/\1/'
    }
    
    detect_platform() {
      local os arch
      os="$(uname -s | tr '[:upper:]' '[:lower:]')"
      arch="$(uname -m)"
    
      case "$os" in
        darwin|linux) ;;
        mingw*|msys*|cygwin*) os="win" ;;
        *) echo "error: unsupported OS: $os" >&2; exit 1 ;;
      esac
    
      case "$arch" in
        arm64|aarch64) arch="arm64" ;;
        x86_64|amd64) arch="x64" ;;
        *) echo "error: unsupported arch: $arch" >&2; exit 1 ;;
      esac
    
      echo "${os}-${arch}"
    }
    
    copy_skill_from_source() {
      local src="$1" dest="$2"
      rm -rf "$dest"
      mkdir -p "$dest"
      tar \
        --exclude='./bin' \
        --exclude='./obj' \
        --exclude='./dist' \
        --exclude='*/bin' \
        --exclude='*/obj' \
        -C "$src" -cf - . | tar -C "$dest" -xf -
    }
    
    install_skill_archive() {
      local tag="$1" dest="$2"
      local tmp
      tmp="$(mktemp -d)"
    
      curl -fsSL "https://github.com/${REPO}/releases/download/${tag}/${SKILL_ASSET}" \
        -o "$tmp/${SKILL_ASSET}"
      rm -rf "$dest"
      mkdir -p "$dest"
      tar -xzf "$tmp/${SKILL_ASSET}" -C "$dest" --strip-components=1
      rm -rf "$tmp"
    }
    
    install_binary() {
      local tag="$1" dest="$2"
      local platform asset archive tmp base binary_name
      platform="$(detect_platform)"
      binary_name="csharp-refactor"
      asset="csharp-refactor-${platform}.tar.gz"
      if [[ "$platform" == win-* ]]; then
        asset="csharp-refactor-${platform}.zip"
        binary_name="csharp-refactor.exe"
      fi
    
      tmp="$(mktemp -d)"
      base="https://github.com/${REPO}/releases/download/${tag}"
      archive="$tmp/$asset"
    
      curl -fsSL "$base/$asset" -o "$archive"
      curl -fsSL "$base/SHA256SUMS" -o "$tmp/SHA256SUMS"
    
      if command -v shasum >/dev/null 2>&1; then
        (cd "$tmp" && shasum -a 256 -c SHA256SUMS --ignore-missing >/dev/null)
      elif command -v sha256sum >/dev/null 2>&1; then
        (cd "$tmp" && sha256sum -c SHA256SUMS --ignore-missing >/dev/null)
      else
        echo "error: shasum or sha256sum is required for checksum verification" >&2
        exit 1
      fi
    
      mkdir -p "$dest/bin"
      case "$asset" in
        *.zip)
          require unzip
          unzip -q "$archive" -d "$tmp/bin"
          ;;
        *.tar.gz)
          tar -xzf "$archive" -C "$tmp"
          ;;
      esac
    
      mv "$tmp/$binary_name" "$dest/bin/$binary_name"
      chmod +x "$dest/bin/$binary_name"
      rm -rf "$tmp"
    }
    
    build_agent_list() {
      if [[ "$all_agents" == 1 ]]; then
        selected_agents=("${AGENT_IDS[@]}")
      elif [[ "$detected_agents" == 1 ]]; then
        selected_agents=()
        local id config_dir
        for id in "${AGENT_IDS[@]}"; do
          config_dir="$(agent_config_dir "$id")"
          if [[ -d "$config_dir" ]]; then
            selected_agents+=("$id")
          fi
        done
      elif [[ ${#selected_agents[@]} -eq 0 ]]; then
        selected_agents=(codex claude-code opencode)
      fi
    }
    
    main() {
      require curl
      require tar
    
      build_agent_list
    
      local tag destinations=() id dest seen_destinations
      seen_destinations="
    "
      for id in "${selected_agents[@]}"; do
        dest="$(agent_skill_dir "$id")"
        if [[ "$seen_destinations" != *"
    $dest
    "* ]]; then
          seen_destinations="${seen_destinations}${dest}
    "
          destinations+=("$dest")
        fi
      done
    
      if [[ ${#destinations[@]} -eq 0 ]]; then
        echo "error: no destination selected" >&2
        exit 1
      fi
    
      tag=""
      if [[ -z "$source_dir" || "$with_binary" == 1 ]]; then
        tag="$(resolve_tag)"
      fi
      if [[ -z "$tag" && -z "$source_dir" ]]; then
        echo "error: no refactoring-csharp-v* release found in ${REPO}" >&2
        exit 1
      fi
    
      for dest in "${destinations[@]}"; do
        echo "Installing ${SKILL_NAME} -> ${dest}"
        if [[ -n "$source_dir" ]]; then
          copy_skill_from_source "$source_dir" "$dest"
        else
          install_skill_archive "$tag" "$dest"
        fi
    
        if [[ "$with_binary" == 1 && -n "$tag" ]]; then
          install_binary "$tag" "$dest"
          echo "  binary: ${dest}/bin/csharp-refactor"
        fi
      done
    
      echo
      echo "Done. Restart any agent that should discover this skill for the first time."
    }
    
    main
    
  • README.md 5.2 KB
    # refactoring-csharp
    
    Inspired by ReSharper. A Roslyn-based refactorer for C# solutions and multi-solution monorepos, packaged as an agent skill with bundled .NET 10 source and optional prebuilt CLI binaries.
    
    ## Contents
    
    - [Quick start](#quick-start)
    - [What it does](#what-it-does)
    - [Supported agents](#supported-agents)
    - [Use directly](#use-directly)
    - [Contract](#contract)
    - [Build and test](#build-and-test)
    - [Release](#release)
    - [License](#license)
    
    ## Quick start
    
    macOS / Linux:
    
    ```bash
    curl -fsSL https://github.com/CodeAlive-AI/ai-driven-development/releases/download/refactoring-csharp-v0.1.0/install-refactoring-csharp.sh | bash
    ```
    
    Windows PowerShell:
    
    ```powershell
    irm https://github.com/CodeAlive-AI/ai-driven-development/releases/download/refactoring-csharp-v0.1.0/install-refactoring-csharp.ps1 | iex
    ```
    
    Default install targets Codex, Claude Code, and OpenCode. Install to a specific agent:
    
    ```bash
    curl -fsSL https://github.com/CodeAlive-AI/ai-driven-development/releases/download/refactoring-csharp-v0.1.0/install-refactoring-csharp.sh | bash -s -- --agent cursor
    ```
    
    ```powershell
    iex "& { $(irm https://github.com/CodeAlive-AI/ai-driven-development/releases/download/refactoring-csharp-v0.1.0/install-refactoring-csharp.ps1) } -Agent cursor"
    ```
    
    Install to all supported global agent skill directories:
    
    ```bash
    curl -fsSL https://github.com/CodeAlive-AI/ai-driven-development/releases/download/refactoring-csharp-v0.1.0/install-refactoring-csharp.sh | bash -s -- --all-agents
    ```
    
    ```powershell
    iex "& { $(irm https://github.com/CodeAlive-AI/ai-driven-development/releases/download/refactoring-csharp-v0.1.0/install-refactoring-csharp.ps1) } -AllAgents"
    ```
    
    Pin a release:
    
    ```bash
    REFACTORING_CSHARP_VERSION=refactoring-csharp-v0.1.0 \
      curl -fsSL https://github.com/CodeAlive-AI/ai-driven-development/releases/download/refactoring-csharp-v0.1.0/install-refactoring-csharp.sh | bash
    ```
    
    ```powershell
    $env:REFACTORING_CSHARP_VERSION = "refactoring-csharp-v0.1.0"
    irm https://github.com/CodeAlive-AI/ai-driven-development/releases/download/refactoring-csharp-v0.1.0/install-refactoring-csharp.ps1 | iex
    ```
    
    ## What it does
    
    - Renames C# symbols across a whole solution using Roslyn.
    - Supports `.sln` and `.slnx`.
    - Supports multi-solution monorepos by accepting a directory, merging discovered solutions/projects into a temporary aggregate `.slnx`, refactoring through Roslyn, and deleting the temporary solution afterward.
    - Resolves targets from `file_path`, 1-based `line_number`, and required `old_name`.
    - Applies by default; dry-run is opt-in.
    - Renames comments by default.
    - Safely moves matching type files by default so git can detect a rename.
    - Uses the target solution's normal MSBuild/Roslyn cache instead of creating a second project cache.
    - Ships source with the skill so agents can build, inspect, patch, and test it locally.
    
    ## Supported agents
    
    The installer follows the global skill directories from the Skills CLI ecosystem and supports:
    
    `adal`, `amp`, `antigravity`, `augment`, `claude-code`, `cline`, `codebuddy`,
    `codex`, `command-code`, `continue`, `crush`, `cursor`, `droid`, `gemini-cli`,
    `github-copilot`, `goose`, `iflow-cli`, `junie`, `kilo`, `kimi-cli`, `kiro-cli`,
    `kode`, `mcpjam`, `mistral-vibe`, `mux`, `neovate`, `openclaw`, `opencode`,
    `openhands`, `pi`, `pochi`, `qoder`, `qwen-code`, `replit`, `roo`, `trae`,
    `trae-cn`, `windsurf`, and `zencoder`.
    
    Useful flags:
    
    - `--agent <id>` / `-Agent <id>` installs to one agent. Repeatable in bash.
    - `--all-agents` / `-AllAgents` installs to every supported global skill directory.
    - `--detected` / `-Detected` installs only to agents whose global config directory exists.
    - `--no-binary` / `-NoBinary` installs only the skill source.
    
    ## Use directly
    
    Preferred, if installed from release with a prebuilt binary:
    
    ```bash
    ~/.codex/skills/refactoring-csharp/bin/csharp-refactor rename-symbol \
      /repo/src/App.slnx \
      /repo/src/Foo.cs \
      42 \
      OldName \
      NewName
    ```
    
    For multi-solution monorepos, pass the repository directory instead of a solution file:
    
    ```bash
    ~/.codex/skills/refactoring-csharp/bin/csharp-refactor rename-symbol \
      /repo \
      /repo/src/Foo.cs \
      42 \
      OldName \
      NewName
    ```
    
    Source fallback:
    
    ```bash
    dotnet run --project ~/.codex/skills/refactoring-csharp/src/CSharpRefactoring.Cli -- \
      rename-symbol /repo/src/App.slnx /repo/src/Foo.cs 42 OldName NewName
    ```
    
    `line_number` is 1-based and can be copied directly from `rg -n`.
    
    ## Contract
    
    ```text
    rename-symbol <sln|slnx|directory> <file> <line> <oldName> <newName> [dryRun=false|true]
    ```
    
    Defaults:
    
    - `dryRun=false`
    - `rename_file=true`
    - `rename_in_comments=true`
    - `rename_in_strings=false`
    - `rename_overloads=false`
    
    ## Build and test
    
    ```bash
    cd skills/refactoring-csharp
    dotnet build src/CSharpRefactoring.slnx
    dotnet test src/CSharpRefactoring.slnx
    ```
    
    ## Release
    
    ```bash
    cd skills/refactoring-csharp
    ./release.sh refactoring-csharp-v0.1.0
    ```
    
    The release script creates:
    
    - `dist/refactoring-csharp-skill.tar.gz`
    - `dist/csharp-refactor-darwin-arm64.tar.gz`
    - `dist/csharp-refactor-darwin-x64.tar.gz`
    - `dist/csharp-refactor-linux-arm64.tar.gz`
    - `dist/csharp-refactor-linux-x64.tar.gz`
    - `dist/csharp-refactor-win-x64.zip`
    - `dist/SHA256SUMS`
    
    ## License
    
    MIT.
    
  • release.sh 2.7 KB
    #!/usr/bin/env bash
    set -euo pipefail
    
    if [[ $# -ne 1 ]]; then
      echo "Usage: ./release.sh refactoring-csharp-vX.Y.Z" >&2
      exit 2
    fi
    
    TAG="$1"
    case "$TAG" in
      refactoring-csharp-v*) ;;
      *) echo "error: tag must start with refactoring-csharp-v" >&2; exit 2 ;;
    esac
    
    SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    DIST_DIR="$SKILL_DIR/dist"
    PROJECT="$SKILL_DIR/src/CSharpRefactoring.Cli/CSharpRefactoring.Cli.csproj"
    
    rm -rf "$DIST_DIR"
    mkdir -p "$DIST_DIR"
    
    make_skill_archive() {
      tar \
        --exclude='./bin' \
        --exclude='./obj' \
        --exclude='./dist' \
        --exclude='./*.ps1xml' \
        --exclude='*/bin' \
        --exclude='*/obj' \
        -C "$SKILL_DIR" \
        -czf "$DIST_DIR/refactoring-csharp-skill.tar.gz" \
        .
    }
    
    copy_installers() {
      cp "$SKILL_DIR/install.sh" "$DIST_DIR/install.sh"
      cp "$SKILL_DIR/install.ps1" "$DIST_DIR/install.ps1"
      cp "$SKILL_DIR/install.sh" "$DIST_DIR/install-refactoring-csharp.sh"
      cp "$SKILL_DIR/install.ps1" "$DIST_DIR/install-refactoring-csharp.ps1"
    }
    
    publish_tar() {
      local rid="$1"
      local asset_platform="$2"
      local out="$DIST_DIR/publish/$rid"
      dotnet publish "$PROJECT" \
        -c Release \
        -r "$rid" \
        --self-contained true \
        -p:PublishSingleFile=true \
        -p:PublishTrimmed=false \
        -p:DebugType=None \
        -p:DebugSymbols=false \
        -o "$out"
      cp "$out/CSharpRefactoring.Cli" "$DIST_DIR/csharp-refactor"
      chmod +x "$DIST_DIR/csharp-refactor"
      tar -C "$DIST_DIR" -czf "$DIST_DIR/csharp-refactor-${asset_platform}.tar.gz" csharp-refactor
      rm "$DIST_DIR/csharp-refactor"
    }
    
    publish_zip() {
      local rid="$1"
      local asset_platform="$2"
      local out="$DIST_DIR/publish/$rid"
      dotnet publish "$PROJECT" \
        -c Release \
        -r "$rid" \
        --self-contained true \
        -p:PublishSingleFile=true \
        -p:PublishTrimmed=false \
        -p:DebugType=None \
        -p:DebugSymbols=false \
        -o "$out"
      cp "$out/CSharpRefactoring.Cli.exe" "$DIST_DIR/csharp-refactor.exe"
      (cd "$DIST_DIR" && zip -q "csharp-refactor-${asset_platform}.zip" csharp-refactor.exe)
      rm "$DIST_DIR/csharp-refactor.exe"
    }
    
    make_skill_archive
    copy_installers
    publish_tar osx-arm64 darwin-arm64
    publish_tar osx-x64 darwin-x64
    publish_tar linux-arm64 linux-arm64
    publish_tar linux-x64 linux-x64
    publish_zip win-x64 win-x64
    
    (
      cd "$DIST_DIR"
      shasum -a 256 \
        refactoring-csharp-skill.tar.gz \
        install.sh \
        install.ps1 \
        install-refactoring-csharp.sh \
        install-refactoring-csharp.ps1 \
        csharp-refactor-darwin-arm64.tar.gz \
        csharp-refactor-darwin-x64.tar.gz \
        csharp-refactor-linux-arm64.tar.gz \
        csharp-refactor-linux-x64.tar.gz \
        csharp-refactor-win-x64.zip \
        > SHA256SUMS
    )
    
    rm -rf "$DIST_DIR/publish"
    
    echo "Release assets created in $DIST_DIR"
    
  • SKILL.md 9.7 KB
    ---
    name: refactoring-csharp
    description: Rename and refactor C# symbols in a .NET solution or multi-solution monorepo with a one-shot Roslyn CLI. Use when the user asks to rename a symbol, preview impact, update references across a solution, or refactor shared projects across several solutions.
    ---
    
    # Refactoring C# Symbols
    
    This skill ships a Roslyn-based C# rename CLI in `src/`. It is intentionally one-shot and
    stateless: one call resolves the target, validates the request, and returns either a preview
    or an applied rename. There is no public prepare step.
    
    ## Contents
    
    - [Canonical CLI](#canonical-cli)
    - [Contract](#contract)
    - [How To Use It](#how-to-use-it)
    - [Monorepo Directory Mode](#monorepo-directory-mode)
    - [Important Behavioral Rules](#important-behavioral-rules)
    - [Supported Targets](#supported-targets)
    - [File Rename Nuance](#file-rename-nuance)
    - [Error Handling](#error-handling)
    - [Success Criteria](#success-criteria)
    - [Recommended Output Style](#recommended-output-style)
    
    ## Canonical CLI
    
    If the skill was installed by `install.sh`, prefer the prebuilt CLI:
    
    ```bash
    bin/csharp-refactor rename-symbol <sln|slnx|directory> <file> <line> <oldName> <newName> [dryRun=false|true]
    ```
    
    Otherwise run the bundled source CLI from the skill directory:
    
    ```bash
    dotnet run --project src/CSharpRefactoring.Cli -- rename-symbol <sln|slnx|directory> <file> <line> <oldName> <newName> [dryRun=false|true]
    ```
    
    When invoked from outside the skill directory, use an absolute project path:
    
    ```bash
    /<skill-dir>/bin/csharp-refactor rename-symbol <sln|slnx|directory> <file> <line> <oldName> <newName> [dryRun=false|true]
    # or, if no prebuilt binary is installed:
    dotnet run --project /path/to/refactoring-csharp/src/CSharpRefactoring.Cli -- rename-symbol <sln|slnx|directory> <file> <line> <oldName> <newName> [dryRun=false|true]
    ```
    
    The tool project may create normal `bin/` and `obj/` directories under the skill. That is
    expected and useful: it lets repeated runs reuse the .NET build cache instead of rebuilding
    the CLI from scratch.
    
    The target solution is not redirected to a temporary build output. The CLI opens the solution
    from the solution directory and lets Roslyn/MSBuild use the target project's normal build
    cache (`obj/`, `bin/`, or the repository's configured artifacts layout). This is intentional:
    it avoids creating a second cache tree for the project being refactored and lets repeated
    renames benefit from the project's existing MSBuild/Roslyn design-time build cache.
    
    ## Contract
    
    | Field | Required | Default | Notes |
    | --- | --- | --- | --- |
    | `solution_path` | yes | - | Absolute path to the `.sln`/`.slnx` file, or a repository directory for monorepo-wide refactoring. |
    | `file_path` | yes | - | Absolute path to a file inside the solution. |
    | `line_number` | yes | - | 1-based line number. Use the value reported by `rg -n`. |
    | `old_name` | yes | - | Exact current identifier on that line. This is the anchor. |
    | `new_name` | yes | - | Must be a valid C# identifier. |
    | `dry_run` | no | `false` | Apply changes by default. Preview only when explicitly set to `true`. |
    | `rename_overloads` | no | `false` | Keep overloads unchanged by default. |
    | `rename_in_strings` | no | `false` | String literals stay untouched by default. |
    | `rename_in_comments` | no | `true` | Comments are renamed by default. |
    | `rename_file` | no | `true` | Safe file move for supported named types. Never recreate the file as delete+add. |
    
    ## How To Use It
    
    1. Use `rg -n` to locate the symbol and copy the 1-based line number directly.
    2. Call `rename-symbol` once without the `dryRun` argument for normal rename requests. This applies the rename.
    3. Use `dryRun=true` only when the user explicitly asks for preview, when the target is ambiguous, or when the rename is unusually broad/risky and applying immediately would be irresponsible. The CLI also accepts `true`, `--dry-run`, `dryRun=false`, `false`, and `--no-dry-run`; invalid values fail fast and must never silently apply.
    4. Do not run a dry run just because the tool supports it. The tool loads the solution on every call, so preview+apply doubles the cost on large projects.
    5. Summarize the result by reporting the original name, new name, changed document count, total text changes, changed files, and any file move.
    
    ## Monorepo Directory Mode
    
    Use directory mode only when a repository contains multiple `.sln`/`.slnx` files or shared
    projects that are not all present in one normal solution. For ordinary repositories, pass the
    specific solution file because it is faster and avoids scanning unrelated projects.
    
    When `solution_path` is a directory:
    
    - Pass the repository or workspace root as `solution_path`.
    - Keep `file_path` as the absolute path to the target source file.
    - The tool recursively discovers `.sln`, `.slnx`, and supported project files under the directory.
    - It merges them into one temporary `.slnx`, opens that aggregate solution, performs the rename, and deletes the temporary solution directory before returning.
    - The temporary aggregate solution lives outside the target repository; source files and normal project build caches remain in their original locations.
    
    ## Important Behavioral Rules
    
    - The tool is stateless. It loads the solution on every call.
    - If `solution_path` is a directory, the tool scans it recursively, merges discovered `.sln`, `.slnx`, and project files into one temporary `.slnx`, opens that solution, runs the rename, and deletes the temporary solution directory before returning.
    - A preview does not reserve state. If the workspace changes between preview and apply, rerun the preview.
    - Prefer one apply call over preview+apply when the user already asked to perform the rename.
    - For a `.sln`/`.slnx` input, the tool runs Roslyn from the target solution directory. For a directory input, it runs Roslyn from that directory while opening the temporary aggregate solution. In both modes, project files stay in place and use the target project's normal MSBuild outputs.
    - Do not invent a session or hidden prepare state.
    - Do not ask for a column number. The tool resolves from `file_path`, `line_number`, and `old_name`.
    - `old_name` is mandatory because it disambiguates the target when a line contains more than one renameable identifier.
    - The bundled source requires .NET 10 and restores NuGet packages on first run.
    - Let the tool keep its own normal `bin/` and `obj/` cache unless the user explicitly asks for a clean/no-cache run.
    - If the tool returns a preview, say preview. If it returns applied changes, say applied.
    - Keep responses concise and action-oriented. Tell the user what changed and whether a file move happened.
    
    ## Supported Targets
    
    Treat these as supported rename targets when the Roslyn symbol is source-backed and
    `CanBeReferencedByName`:
    
    - `NamedType`
    - `Method`
    - `Property`
    - `Field`
    - `Event`
    - `Parameter`
    - `Local`
    - `TypeParameter`
    - `Namespace`
    
    Do not rename constructors, destructors, static constructors, or indexers.
    
    ## File Rename Nuance
    
    `rename_file=true` is a convenience default, but it only produces a real safe move when the
    symbol is a single-declaration named type and the file stem matches the current type name.
    
    If the tool does not return `file_move_from_path` and `file_move_to_path`, the symbol rename is still valid,
    but the file itself was not moved. Do not claim a file rename happened unless the tool reports it.
    
    This is intentionally conservative so git sees a tracked rename instead of a delete+add pair.
    
    ## Error Handling
    
    Use the tool's error codes as actionable guidance:
    
    | Error code | Meaning | What to do |
    | --- | --- | --- |
    | `invalid_solution_path` | Solution path is missing, does not exist, or is neither `.sln`/`.slnx` nor a directory. | Ask for a real solution path or repository directory. |
    | `invalid_file_path` | File path is missing or not present on disk. | Ask for the correct file path. |
    | `file_not_in_solution` | The file is not part of the loaded solution. | Ask for the correct file or solution. |
    | `invalid_line_number` | Line number is outside file bounds or not 1-based. | Ask for the correct line. |
    | `invalid_old_name` | `old_name` was empty or whitespace. | Ask for the exact current name. |
    | `old_name_not_found_on_line` | No renameable symbol with that name exists on the line. | Ask for a better line or file. |
    | `ambiguous_old_name_on_line` | More than one renameable symbol matches that name on the line. | Narrow the target or use a different line. |
    | `unsupported_symbol_kind` | Roslyn found a symbol, but this kind is not renameable here. | Move to a supported symbol kind. |
    | `symbol_not_in_source` | The symbol is not declared in source. | Pick a source-backed target. |
    | `invalid_new_name` | `new_name` is not a valid C# identifier. | Propose a valid identifier. |
    | `same_name` | New name equals the current name. | Ask for a different name. |
    | `no_changes` | Roslyn produced no text edits. | Re-check the target or the new name. |
    | `apply_failed` | Workspace apply failed. | Treat as a runtime failure and retry only if the state is unchanged. |
    | `operation_timeout` | The rename timed out. | Retry with a larger timeout or a narrower target. |
    
    ## Success Criteria
    
    A rename workflow is complete when:
    
    - The target was resolved from `line_number` + `old_name`.
    - The user approved the rename, or explicitly requested a dry run only.
    - The tool returned changed documents, total text changes, and any file move details.
    - The final answer makes the applied scope clear enough for the user to trust the change.
    
    ## Recommended Output Style
    
    - For previews, say what would change and that nothing was applied.
    - For applied changes, say what changed and whether the file was moved.
    - If the file move fields are present, mention them explicitly.
    - If the tool returned an error code, echo the code and the human-readable reason.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related