diff --git a/.github/dependabot-dotnet.yml b/.github/dependabot-dotnet.yml
new file mode 100644
index 000000000..831b7643f
--- /dev/null
+++ b/.github/dependabot-dotnet.yml
@@ -0,0 +1,20 @@
+version: 2
+updates:
+ - package-ecosystem: "nuget"
+ directory: "/sdk/dotnet"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
+ labels:
+ - "dependencies"
+ commit-message:
+ prefix: "chore"
+ include: "scope"
+
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ labels:
+ - "dependencies"
+ - "github-actions"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6f292fc4e..b11e5c0c3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -63,3 +63,32 @@ jobs:
test -f dist/index.cjs
test -f dist/index.d.ts
test -f dist/index.d.cts
+
+ dotnet-sdk:
+ name: .NET SDK Build & Test
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
+ with:
+ dotnet-version: "8.0.x"
+
+ - name: Cache NuGet packages
+ uses: actions/cache@v4
+ with:
+ path: ~/.nuget/packages
+ key: ${{ runner.os }}-nuget-${{ hashFiles('sdk/dotnet/**/Directory.Packages.props') }}
+ restore-keys: |
+ ${{ runner.os }}-nuget-
+
+ - name: Restore
+ run: dotnet restore sdk/dotnet/RuleSync.Sdk.DotNet.sln
+
+ - name: Build
+ run: dotnet build sdk/dotnet/RuleSync.Sdk.DotNet.sln --configuration Release --no-restore
+
+ - name: Test
+ run: dotnet test sdk/dotnet/RuleSync.Sdk.DotNet.sln --configuration Release --no-build --verbosity normal
diff --git a/.github/workflows/publish-assets.yml b/.github/workflows/publish-assets.yml
index 10bc64f9c..bb09ed7e8 100644
--- a/.github/workflows/publish-assets.yml
+++ b/.github/workflows/publish-assets.yml
@@ -78,6 +78,23 @@ jobs:
EVENT_NAME: ${{ github.event_name }}
HEAD_REF: ${{ github.event.pull_request.head.ref }}
+ - name: Setup .NET
+ uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
+ with:
+ dotnet-version: "8.0.x"
+
+ - name: Build .NET SDK
+ run: |
+ dotnet restore sdk/dotnet/RuleSync.Sdk.DotNet.sln
+ dotnet build sdk/dotnet/RuleSync.Sdk.DotNet.sln --configuration Release --no-restore
+
+ - name: Pack .NET SDK
+ env:
+ RELEASE_TAG: ${{ steps.tag.outputs.tag }}
+ run: |
+ VERSION="${RELEASE_TAG#v}"
+ dotnet pack sdk/dotnet/src/RuleSync.Sdk.DotNet/RuleSync.Sdk.DotNet.csproj --configuration Release --no-build --output ./nupkgs -p:Version="$VERSION"
+
- name: Upload assets to release
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:
@@ -94,3 +111,4 @@ jobs:
repomix-toon.zip
config-schema.json
mcp-schema.json
+ nupkgs/*.nupkg
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 6928fcd7a..c16afc483 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -8,6 +8,7 @@ on:
permissions:
id-token: write # Required for OIDC
contents: write # Required for checking out code and editing release state
+ packages: write # Required for publishing to GitHub Packages
jobs:
publish:
@@ -32,14 +33,80 @@ jobs:
- name: Publish to npm
run: pnpm publish
- - name: Publish target draft release
+ - name: Setup .NET
+ uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
+ with:
+ dotnet-version: "8.0.x"
+
+ - name: Validate actor and extract tag
+ id: tag
env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
+ GITHUB_ACTOR: ${{ github.actor }}
run: |
+ # Validate github.actor format (prevent potential script injection)
+ if ! echo "$GITHUB_ACTOR" | grep -qE '^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$'; then
+ echo "::error::Invalid github.actor format: $GITHUB_ACTOR"
+ exit 1
+ fi
+
TAG="${HEAD_BRANCH#release/}"
if ! echo "$TAG" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "::error::Invalid release tag derived from branch: $HEAD_BRANCH"
exit 1
fi
+ echo "tag=$TAG" >> "$GITHUB_OUTPUT"
+
+ - name: Download NuGet package from release
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ TAG: ${{ steps.tag.outputs.tag }}
+ run: |
+ mkdir -p nupkgs
+ gh release download "$TAG" --pattern "*.nupkg" --dir nupkgs
+
+ - name: Sign NuGet packages
+ env:
+ NUGET_CERTIFICATE: ${{ secrets.NUGET_SIGNING_CERTIFICATE }}
+ NUGET_CERTIFICATE_PASSWORD: ${{ secrets.NUGET_SIGNING_CERTIFICATE_PASSWORD }}
+ run: |
+ set +x # Disable command echo to prevent leaking secrets
+ if [ -n "$NUGET_CERTIFICATE" ]; then
+ # Create secure temp directory with restricted permissions
+ CERT_DIR=$(mktemp -d)
+ chmod 700 "$CERT_DIR"
+ CERT_PATH="$CERT_DIR/certificate.pfx"
+ echo "$NUGET_CERTIFICATE" | base64 -d > "$CERT_PATH"
+ chmod 600 "$CERT_PATH"
+
+ for pkg in nupkgs/*.nupkg; do
+ dotnet nuget sign "$pkg" --certificate-path "$CERT_PATH" --certificate-password "$NUGET_CERTIFICATE_PASSWORD" --timestamper http://timestamp.digicert.com
+ done
+
+ # Secure cleanup
+ shred -u "$CERT_PATH" 2>/dev/null || rm -f "$CERT_PATH"
+ rmdir "$CERT_DIR"
+ else
+ echo "Warning: NUGET_SIGNING_CERTIFICATE not configured. Packages will be published unsigned."
+ fi
+
+ - name: Publish to GitHub Packages
+ env:
+ GITHUB_ACTOR: ${{ github.actor }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ REPO_OWNER: ${{ github.repository_owner }}
+ run: |
+ # Validate REPO_OWNER to prevent script injection
+ if ! echo "$REPO_OWNER" | grep -qE '^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$'; then
+ echo "::error::Invalid REPO_OWNER format: $REPO_OWNER"
+ exit 1
+ fi
+ dotnet nuget add source "https://nuget.pkg.github.com/${REPO_OWNER}/index.json" --name github --username "$GITHUB_ACTOR" --password "$GITHUB_TOKEN" --store-password-in-clear-text
+ dotnet nuget push nupkgs/*.nupkg --source github --skip-duplicate
+
+ - name: Publish target draft release
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ TAG: ${{ steps.tag.outputs.tag }}
+ run: |
gh release edit "$TAG" --draft=false --latest
diff --git a/cspell.json b/cspell.json
index 66abe161d..a6e4a9caa 100644
--- a/cspell.json
+++ b/cspell.json
@@ -175,10 +175,12 @@
"msvc",
"musleabihf",
"namespacing",
+ "netstandard",
"newai",
"newtool",
"newtoolignore",
"noai",
+ "NETSTANDARD",
"nomodel",
"Nonoctal",
"nonroot",
@@ -265,6 +267,7 @@
"tlsv",
"toidentifier",
"Toon",
+ "tooltarget",
"toon",
"TOON",
"toolname",
@@ -293,6 +296,8 @@
"wrappy",
"wscript",
"XVCJ",
+ "xunit",
+ "Xunit",
"zhipuai",
"русский",
"файл",
@@ -324,6 +329,10 @@
"KAKEHASHI",
"redirs",
"symref",
- "TOCTOU"
+ "TOCTOU",
+ "augmentcode-legacy",
+ "claudecode-legacy",
+ "fetc",
+ "fpasswd"
]
}
diff --git a/sdk/dotnet/.editorconfig b/sdk/dotnet/.editorconfig
new file mode 100644
index 000000000..a62429f58
--- /dev/null
+++ b/sdk/dotnet/.editorconfig
@@ -0,0 +1,142 @@
+root = true
+
+# All files
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 4
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+# C# files
+[*.cs]
+# Indentation
+indent_size = 4
+tab_width = 4
+
+# New lines
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_between_query_expression_clauses = true
+
+# Spaces
+csharp_space_after_cast = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_between_parentheses = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+
+# Wrapping
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+# Organize usings
+dotnet_sort_system_directives_first = true
+dotnet_separate_import_directive_groups = false
+
+# Naming conventions
+dotnet_naming_style.pascal_case_style.capitalization = pascal_case
+dotnet_naming_style.camel_case_style.capitalization = camel_case
+
+# Classes, structs, enums, delegates: PascalCase
+dotnet_naming_symbols.types.applicable_kinds = class, struct, enum, delegate, interface
+dotnet_naming_rule.types_naming.symbols = types
+dotnet_naming_rule.types_naming.style = pascal_case_style
+dotnet_naming_rule.types_naming.severity = suggestion
+
+# Constants: PascalCase
+dotnet_naming_symbols.constants.applicable_kinds = field
+dotnet_naming_symbols.constants.applicable_accessibilities = *
+dotnet_naming_symbols.constants.required_modifiers = const
+dotnet_naming_rule.constants_naming.symbols = constants
+dotnet_naming_rule.constants_naming.style = pascal_case_style
+dotnet_naming_rule.constants_naming.severity = suggestion
+
+# Static fields: PascalCase
+dotnet_naming_symbols.static_fields.applicable_kinds = field
+dotnet_naming_symbols.static_fields.applicable_accessibilities = *
+dotnet_naming_symbols.static_fields.required_modifiers = static
+dotnet_naming_rule.static_fields_naming.symbols = static_fields
+dotnet_naming_rule.static_fields_naming.style = pascal_case_style
+dotnet_naming_rule.static_fields_naming.severity = suggestion
+
+# Instance fields: camelCase with underscore prefix
+dotnet_naming_style.instance_field_style.capitalization = camel_case
+dotnet_naming_style.instance_field_style.required_prefix = _
+dotnet_naming_symbols.instance_fields.applicable_kinds = field
+dotnet_naming_symbols.instance_fields.applicable_accessibilities = private
+dotnet_naming_rule.instance_fields_naming.symbols = instance_fields
+dotnet_naming_rule.instance_fields_naming.style = instance_field_style
+dotnet_naming_rule.instance_fields_naming.severity = suggestion
+
+# Parameters, locals: camelCase
+dotnet_naming_symbols.parameters_locals.applicable_kinds = parameter, local
+dotnet_naming_rule.parameters_locals_naming.symbols = parameters_locals
+dotnet_naming_rule.parameters_locals_naming.style = camel_case_style
+dotnet_naming_rule.parameters_locals_naming.severity = suggestion
+
+# Expression-bodied members
+csharp_style_expression_bodied_methods = when_on_single_line:suggestion
+csharp_style_expression_bodied_constructors = when_on_single_line:suggestion
+csharp_style_expression_bodied_operators = when_on_single_line:suggestion
+csharp_style_expression_bodied_properties = when_on_single_line:suggestion
+csharp_style_expression_bodied_indexers = when_on_single_line:suggestion
+csharp_style_expression_bodied_accessors = when_on_single_line:suggestion
+csharp_style_expression_bodied_lambdas = when_on_single_line:suggestion
+csharp_style_expression_bodied_local_functions = when_on_single_line:suggestion
+
+# Pattern matching
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+
+# Var preferences
+csharp_style_var_for_built_in_types = true:suggestion
+csharp_style_var_when_type_is_apparent = true:suggestion
+csharp_style_var_elsewhere = true:suggestion
+
+# Null checking
+csharp_style_throw_expression = true:suggestion
+csharp_style_conditional_delegate_call = true:suggestion
+
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
+csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion
+
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion
+
+# Roslyn analyzer settings
+dotnet_diagnostic.CA1822.severity = suggestion # Mark members as static
+dotnet_diagnostic.CA2201.severity = none # Do not raise reserved exception types
+dotnet_diagnostic.IDE0005.severity = warning # Using directive is unnecessary
+
+# XML documentation
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
+
+# Project files
+[*.{csproj,props,targets}]
+indent_size = 2
+
+# JSON files
+[*.{json,jsonc}]
+indent_size = 2
+
+# YAML files
+[*.{yml,yaml}]
+indent_size = 2
+
+# Markdown files
+[*.md]
+trim_trailing_whitespace = false
diff --git a/sdk/dotnet/.gitignore b/sdk/dotnet/.gitignore
new file mode 100644
index 000000000..40ec6a9c2
--- /dev/null
+++ b/sdk/dotnet/.gitignore
@@ -0,0 +1,28 @@
+# .NET SDK generated files
+bin/
+obj/
+
+# Test coverage reports
+coverage-report/
+TestResults/
+
+# NuGet
+*.nupkg
+*.snupkg
+
+# User-specific files
+*.user
+*.userosscache
+*.suo
+*.swp
+*.*~
+
+# IDEs
+.vs/
+.vscode/
+.idea/
+
+# Coverage tools
+*.coverage
+*.coveragexml
+*.opencover.xml
diff --git a/sdk/dotnet/Directory.Packages.props b/sdk/dotnet/Directory.Packages.props
new file mode 100644
index 000000000..c2e48c038
--- /dev/null
+++ b/sdk/dotnet/Directory.Packages.props
@@ -0,0 +1,31 @@
+
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sdk/dotnet/README.md b/sdk/dotnet/README.md
new file mode 100644
index 000000000..8cff5881c
--- /dev/null
+++ b/sdk/dotnet/README.md
@@ -0,0 +1,296 @@
+# RuleSync.Sdk.DotNet
+
+A C# SDK for [rulesync](https://github.com/dyoshikawa/rulesync) - generate AI tool configurations programmatically from .NET applications.
+
+## Features
+
+- **Multi-target support**: .NET Standard 2.1, .NET 6.0, .NET 8.0
+- **Source-generated types**: Types are auto-generated from rulesync's TypeScript source at compile time
+- **Result pattern**: Functional error handling with `Result`
+- **Async/await**: `ValueTask` for efficient async operations
+
+## Installation
+
+### From GitHub Packages
+
+```bash
+# Add GitHub Packages source (replace USERNAME with your GitHub username)
+dotnet nuget add source "https://nuget.pkg.github.com/dyoshikawa/index.json" \
+ --name "github" \
+ --username USERNAME \
+ --password YOUR_GITHUB_TOKEN
+
+# Install the package
+dotnet add package RuleSync.Sdk.DotNet --source github
+```
+
+Or add to your `nuget.config`:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+**Note**: You need a GitHub personal access token with `read:packages` scope.
+
+### From Source
+
+```bash
+git clone https://github.com/dyoshikawa/rulesync.git
+cd rulesync/sdk/dotnet
+dotnet pack src/RuleSync.Sdk.DotNet/RuleSync.Sdk.DotNet.csproj -c Release
+dotnet add package RuleSync.Sdk.DotNet --source ./src/RuleSync.Sdk.DotNet/bin/Release
+```
+
+## Prerequisites
+
+- Node.js 20+ installed and available in PATH
+- rulesync npm package (used via npx)
+
+## Quick Start
+
+```csharp
+using RuleSync.Sdk;
+using RuleSync.Sdk.Models;
+
+// Create client
+using var client = new RulesyncClient();
+
+// Generate configurations for specific targets
+var result = await client.GenerateAsync(new GenerateOptions
+{
+ Targets = new[] { ToolTarget.ClaudeCode, ToolTarget.Cursor },
+ Features = new[] { Feature.Rules, Feature.Mcp }
+});
+
+if (result.IsSuccess)
+{
+ Console.WriteLine($"Generated configs");
+}
+else
+{
+ Console.WriteLine($"Error: {result.Error}");
+}
+```
+
+## API Reference
+
+### RulesyncClient
+
+```csharp
+public sealed class RulesyncClient : IDisposable
+{
+ // Uses "node" from PATH, 60 second timeout
+ public RulesyncClient();
+
+ // Custom configuration
+ public RulesyncClient(
+ string? nodeExecutablePath = null, // Path to node executable
+ string? rulesyncPath = null, // Path to rulesync package (null = use npx)
+ TimeSpan? timeout = null); // Operation timeout
+
+ // Generate AI tool configurations
+ public ValueTask> GenerateAsync(
+ GenerateOptions? options = null,
+ CancellationToken cancellationToken = default);
+
+ // Import configuration from existing AI tool
+ public ValueTask> ImportAsync(
+ ImportOptions options,
+ CancellationToken cancellationToken = default);
+}
+```
+
+### Result
+
+```csharp
+public readonly struct Result
+{
+ public bool IsSuccess { get; }
+ public bool IsFailure { get; }
+ public T Value { get; } // Throws if failed
+ public RulesyncError Error { get; } // Throws if success
+
+ public Result Map(Func mapper);
+ public Result OnSuccess(Action action);
+ public Result OnFailure(Action action);
+}
+```
+
+### Source-Generated Types
+
+Types are automatically generated from rulesync's TypeScript source at compile time:
+
+**Feature enum** (from `src/types/features.ts`):
+
+- `Rules`, `Ignore`, `Mcp`, `Subagents`, `Commands`, `Skills`, `Hooks`
+
+**ToolTarget enum** (from `src/types/tool-targets.ts`):
+
+- `Agentsmd`, `Agentsskills`, `Antigravity`, `Augmentcode`, `Claudecode`, `Codexcli`, `Copilot`, `Cursor`, `Factorydroid`, `Geminicli`, `Goose`, `Junie`, `Kilo`, `Kiro`, `Opencode`, `Qwencode`, `Replit`, `Roo`, `Warp`, `Windsurf`, `Zed`
+
+**GenerateOptions** (from `src/lib/generate.ts`):
+
+```csharp
+public sealed class GenerateOptions
+{
+ public IReadOnlyList? Targets { get; init; }
+ public IReadOnlyList? Features { get; init; }
+ public bool? Verbose { get; init; }
+ public bool? Silent { get; init; }
+ public bool? Delete { get; init; }
+ public bool? Global { get; init; }
+ public bool? SimulateCommands { get; init; }
+ public bool? SimulateSubagents { get; init; }
+ public bool? SimulateSkills { get; init; }
+ public bool? DryRun { get; init; }
+ public bool? Check { get; init; }
+}
+```
+
+**ImportOptions** (from `src/lib/import.ts`):
+
+```csharp
+public sealed class ImportOptions
+{
+ public ToolTarget Target { get; init; } // Required
+ public IReadOnlyList? Features { get; init; }
+ public bool? Verbose { get; init; }
+ public bool? Silent { get; init; }
+ public bool? Global { get; init; }
+}
+```
+
+## Examples
+
+### Generate all features for all tools
+
+```csharp
+var result = await client.GenerateAsync();
+```
+
+### Generate specific features for Cursor
+
+```csharp
+var result = await client.GenerateAsync(new GenerateOptions
+{
+ Targets = new[] { ToolTarget.Cursor },
+ Features = new[] { Feature.Rules, Feature.Mcp, Feature.Skills }
+});
+```
+
+### Import from Claude Code
+
+```csharp
+var result = await client.ImportAsync(new ImportOptions
+{
+ Target = ToolTarget.Claudecode,
+ Features = new[] { Feature.Rules, Feature.Mcp }
+});
+
+if (result.IsSuccess)
+{
+ Console.WriteLine("Import successful");
+}
+```
+
+### Error handling
+
+```csharp
+var result = await client.GenerateAsync(options);
+
+result
+ .OnSuccess(r => Console.WriteLine("Generated successfully"))
+ .OnFailure(e => Console.WriteLine($"Error {e.Code}: {e.Message}"));
+
+// Or use pattern matching
+if (result.IsSuccess)
+{
+ var value = result.Value;
+}
+else
+{
+ var error = result.Error;
+}
+```
+
+### Custom timeout and paths
+
+```csharp
+using var client = new RulesyncClient(
+ nodeExecutablePath: "/usr/local/bin/node",
+ rulesyncPath: "/path/to/rulesync", // Local rulesync installation
+ timeout: TimeSpan.FromMinutes(2)
+);
+```
+
+## How It Works
+
+The SDK uses an **incremental source generator** that parses rulesync's TypeScript type definitions at compile time and generates corresponding C# types:
+
+1. **TypeScript parsing**: The generator reads TypeScript files from the rulesync source:
+ - `src/types/features.ts` → `Feature` enum
+ - `src/types/tool-targets.ts` → `ToolTarget` enum
+ - `src/lib/generate.ts` → `GenerateOptions`, `GenerateResult`
+ - `src/lib/import.ts` → `ImportOptions`, `ImportResult`
+
+2. **Compile-time generation**: Types are generated during build, not runtime
+
+3. **IDE support**: Full IntelliSense and autocomplete for all generated types
+
+## Building from Source
+
+```bash
+# Clone the repository
+git clone https://github.com/dyoshikawa/rulesync.git
+cd rulesync/sdk/dotnet
+
+# Build
+dotnet build
+
+# Pack as NuGet package
+dotnet pack -c Release
+```
+
+## Architecture
+
+```
+RulesyncClient
+ |
+ +-- Spawns Node.js process
+ | +-- npx rulesync generate ...
+ | +-- npx rulesync import ...
+ |
+ +-- JSON output parsing
+ | +-- System.Text.Json
+ |
+ +-- Returns Result
+
+Source Generator
+ |
+ +-- Parses TypeScript files at compile time
+ | +-- src/types/features.ts
+ | +-- src/types/tool-targets.ts
+ | +-- src/lib/generate.ts
+ | +-- src/lib/import.ts
+ |
+ +-- Generates C# types
+ +-- Feature enum
+ +-- ToolTarget enum
+ +-- GenerateOptions class
+ +-- ImportOptions class
+```
+
+## License
+
+MIT License - see LICENSE file for details.
diff --git a/sdk/dotnet/RuleSync.Sdk.DotNet.sln b/sdk/dotnet/RuleSync.Sdk.DotNet.sln
new file mode 100644
index 000000000..ae9db1e38
--- /dev/null
+++ b/sdk/dotnet/RuleSync.Sdk.DotNet.sln
@@ -0,0 +1,69 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RuleSync.Sdk.DotNet", "src\RuleSync.Sdk.DotNet\RuleSync.Sdk.DotNet.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RuleSync.Sdk.DotNet.SourceGenerators", "src\RuleSync.Sdk.DotNet.SourceGenerators\RuleSync.Sdk.DotNet.SourceGenerators.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RuleSync.Sdk.DotNet.Tests", "tests\RuleSync.Sdk.DotNet.Tests\RuleSync.Sdk.DotNet.Tests.csproj", "{6B09B36F-A8ED-4DE5-8774-09725BEDB101}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.Build.0 = Debug|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.Build.0 = Release|Any CPU
+ {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x64.Build.0 = Debug|Any CPU
+ {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x86.Build.0 = Debug|Any CPU
+ {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x64.ActiveCfg = Release|Any CPU
+ {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x64.Build.0 = Release|Any CPU
+ {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x86.ActiveCfg = Release|Any CPU
+ {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x86.Build.0 = Release|Any CPU
+ {6B09B36F-A8ED-4DE5-8774-09725BEDB101}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6B09B36F-A8ED-4DE5-8774-09725BEDB101}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6B09B36F-A8ED-4DE5-8774-09725BEDB101}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6B09B36F-A8ED-4DE5-8774-09725BEDB101}.Debug|x64.Build.0 = Debug|Any CPU
+ {6B09B36F-A8ED-4DE5-8774-09725BEDB101}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6B09B36F-A8ED-4DE5-8774-09725BEDB101}.Debug|x86.Build.0 = Debug|Any CPU
+ {6B09B36F-A8ED-4DE5-8774-09725BEDB101}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6B09B36F-A8ED-4DE5-8774-09725BEDB101}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6B09B36F-A8ED-4DE5-8774-09725BEDB101}.Release|x64.ActiveCfg = Release|Any CPU
+ {6B09B36F-A8ED-4DE5-8774-09725BEDB101}.Release|x64.Build.0 = Release|Any CPU
+ {6B09B36F-A8ED-4DE5-8774-09725BEDB101}.Release|x86.ActiveCfg = Release|Any CPU
+ {6B09B36F-A8ED-4DE5-8774-09725BEDB101}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {6B09B36F-A8ED-4DE5-8774-09725BEDB101} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
+ EndGlobalSection
+EndGlobal
diff --git a/sdk/dotnet/nuget.config b/sdk/dotnet/nuget.config
new file mode 100644
index 000000000..765346e53
--- /dev/null
+++ b/sdk/dotnet/nuget.config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/sdk/dotnet/src/RuleSync.Sdk.DotNet.SourceGenerators/RuleSync.Sdk.DotNet.SourceGenerators.csproj b/sdk/dotnet/src/RuleSync.Sdk.DotNet.SourceGenerators/RuleSync.Sdk.DotNet.SourceGenerators.csproj
new file mode 100644
index 000000000..e6a08ea25
--- /dev/null
+++ b/sdk/dotnet/src/RuleSync.Sdk.DotNet.SourceGenerators/RuleSync.Sdk.DotNet.SourceGenerators.csproj
@@ -0,0 +1,26 @@
+
+
+ netstandard2.0
+ 12.0
+ enable
+ disable
+
+
+ false
+ true
+ true
+ true
+
+
+ RuleSync.Sdk.DotNet.SourceGenerators
+ RuleSync.Sdk.SourceGenerators
+ RuleSync.Sdk.DotNet.SourceGenerators
+ 7.15.2
+ Incremental source generators for RuleSync SDK
+
+
+
+
+
+
+
diff --git a/sdk/dotnet/src/RuleSync.Sdk.DotNet.SourceGenerators/RulesyncIncrementalGenerator.cs b/sdk/dotnet/src/RuleSync.Sdk.DotNet.SourceGenerators/RulesyncIncrementalGenerator.cs
new file mode 100644
index 000000000..082296f4b
--- /dev/null
+++ b/sdk/dotnet/src/RuleSync.Sdk.DotNet.SourceGenerators/RulesyncIncrementalGenerator.cs
@@ -0,0 +1,567 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using Microsoft.CodeAnalysis;
+
+namespace RuleSync.Sdk.SourceGenerators;
+
+///
+/// Incremental source generator that parses TypeScript type definitions
+/// and generates corresponding C# types.
+///
+[Generator]
+public class RulesyncIncrementalGenerator : IIncrementalGenerator
+{
+ // Regex patterns cached for performance
+ // Matches: export const ALL_FEATURES = ["a", "b", "c"] as const;
+ private static readonly Regex ConstArrayRegex = new(
+ @"export\s+const\s+ALL_(\w+)\s*=\s*\[([^\]]*)\]",
+ RegexOptions.Compiled | RegexOptions.Singleline);
+
+ // Matches: export type Feature = "a" | "b" | "c";
+ private static readonly Regex UnionTypeRegex = new(
+ @"export\s+type\s+(\w+)\s*=\s*([^;]+);",
+ RegexOptions.Compiled | RegexOptions.Singleline);
+
+ // Matches: export type Foo = { ... } (but NOT export type { Foo } from "...")
+ private static readonly Regex ObjectTypeRegex = new(
+ @"export\s+type\s+(\w+)\s*=\s*\{([^{}]*)\}",
+ RegexOptions.Compiled | RegexOptions.Singleline);
+
+ private static readonly Regex PropertyRegex = new(
+ @"(\w+)(\??):\s*([^;\n]+);?",
+ RegexOptions.Compiled | RegexOptions.Multiline);
+
+ ///
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ // Step 1: Get TypeScript files from AdditionalTextsProvider
+ var typeScriptFiles = context.AdditionalTextsProvider
+ .Where(static (file) => file.Path.EndsWith(".ts", StringComparison.OrdinalIgnoreCase))
+ .Select(static (file, ct) =>
+ {
+ var text = file.GetText(ct);
+ return text?.ToString() ?? string.Empty;
+ });
+
+ // Step 2: Get analyzer config options (for RulesyncSourcePath)
+ var configOptions = context.AnalyzerConfigOptionsProvider
+ .Select(static (options, ct) =>
+ {
+ options.GlobalOptions.TryGetValue(
+ "build_property.RulesyncSourcePath",
+ out var sourcePath);
+ return sourcePath ?? string.Empty;
+ });
+
+ // Step 3: Combine TypeScript content with config
+ var combined = typeScriptFiles.Collect()
+ .Combine(configOptions);
+
+ // Step 4: Register source output
+ context.RegisterSourceOutput(combined, (spc, source) =>
+ {
+ var (contents, sourcePath) = source;
+ GenerateCode(spc, contents, sourcePath);
+ });
+ }
+
+ private static void GenerateCode(
+ SourceProductionContext context,
+ ImmutableArray fileContents,
+ string sourcePath)
+ {
+ var enums = new Dictionary();
+ var records = new Dictionary();
+
+ foreach (var content in fileContents)
+ {
+ ParseConstArrays(content, enums);
+ ParseUnionTypes(content, enums);
+ ParseObjectTypes(content, records);
+ }
+
+ // Track already-added hint names to avoid duplicates
+ var addedHintNames = new HashSet();
+
+ // Generate enums
+ foreach (var enumDef in enums.Values)
+ {
+ var hintName = $"{enumDef.Name}.g.cs";
+ if (addedHintNames.Add(hintName))
+ {
+ var source = GenerateEnumSource(enumDef);
+ context.AddSource(hintName, source);
+ }
+ }
+
+ // Generate records
+ foreach (var recordDef in records.Values)
+ {
+ var hintName = $"{recordDef.Name}.g.cs";
+ if (addedHintNames.Add(hintName))
+ {
+ var source = GenerateRecordSource(recordDef, context);
+ context.AddSource(hintName, source);
+ }
+ }
+
+ // Generate IsExternalInit polyfill for netstandard2.1
+ if (addedHintNames.Add("IsExternalInit.g.cs"))
+ {
+ var isExternalInit = GenerateIsExternalInitSource();
+ context.AddSource("IsExternalInit.g.cs", isExternalInit);
+ }
+ }
+
+ private static bool IsKnownType(string typeName)
+ {
+ // Skip complex types that aren't generated
+ var knownPrimitiveTypes = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "string", "int", "bool", "number", "boolean", "object"
+ };
+
+ return knownPrimitiveTypes.Contains(typeName) ||
+ typeName.Contains("[]") || // Arrays
+ typeName.StartsWith("IReadOnlyList<");
+ }
+
+ private static string GenerateIsExternalInitSource()
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("// ");
+ sb.AppendLine("#nullable enable");
+ sb.AppendLine();
+ sb.AppendLine("namespace System.Runtime.CompilerServices;");
+ sb.AppendLine();
+ sb.AppendLine("/// Polyfill for init-only properties on older frameworks.");
+ sb.AppendLine("internal static class IsExternalInit { }");
+ return sb.ToString();
+ }
+
+ private static void ParseConstArrays(string content, Dictionary enums)
+ {
+ foreach (Match match in ConstArrayRegex.Matches(content))
+ {
+ var name = match.Groups[1].Value; // e.g., "FEATURES" from ALL_FEATURES
+ var valuesText = match.Groups[2].Value;
+
+ // Parse array values: "rules", "ignore", "mcp", ...
+ var values = valuesText.Split(',')
+ .Select(v => v.Trim())
+ .Where(v => !string.IsNullOrEmpty(v))
+ .Where(v => !v.StartsWith("...")) // Skip spread operators
+ .Select(v => v.Trim('"', '\''))
+ .Where(v => !string.IsNullOrEmpty(v))
+ .ToList();
+
+ if (values.Count > 0)
+ {
+ // Convert ALL_FEATURES -> Feature
+ var typeName = ToSingular(name);
+
+ // Skip if already added
+ if (enums.ContainsKey(typeName))
+ continue;
+
+ var enumValues = values
+ .Select(v => new EnumValue(v, ToPascalCase(v)))
+ .ToImmutableArray();
+
+ enums[typeName] = new EnumDefinition(typeName, enumValues);
+ }
+ }
+ }
+
+ private static string ToSingular(string plural)
+ {
+ // Convert to PascalCase and remove trailing 's' or 'S'
+ var pascal = ToPascalCase(plural.ToLowerInvariant());
+ if (pascal.EndsWith("s"))
+ return pascal.Substring(0, pascal.Length - 1);
+ return pascal;
+ }
+
+ private static void ParseUnionTypes(string content, Dictionary enums)
+ {
+ foreach (Match match in UnionTypeRegex.Matches(content))
+ {
+ var name = match.Groups[1].Value;
+
+ // Skip if already added
+ if (enums.ContainsKey(name))
+ continue;
+
+ var values = match.Groups[2].Value;
+
+ // Skip z.infer and other complex type patterns
+ if (values.Contains("z.infer") || values.Contains("typeof"))
+ continue;
+
+ // Skip if contains angle brackets (generic types like Partial<...>)
+ if (values.Contains("<") || values.Contains(">"))
+ continue;
+
+ // Skip if doesn't look like a string union (must have quotes)
+ if (!values.Contains("\""))
+ continue;
+
+ // Parse union values: "value1" | "value2" | "value3"
+ var enumValues = values.Split('|')
+ .Select(v => v.Trim().Trim('"', '\''))
+ .Where(v => !string.IsNullOrEmpty(v))
+ .Where(v => !v.StartsWith("...")) // Skip spread operators
+ .Select(v => new EnumValue(
+ v,
+ ToPascalCase(v)))
+ .ToImmutableArray();
+
+ if (enumValues.Length > 0)
+ {
+ enums[name] = new EnumDefinition(name, enumValues);
+ }
+ }
+ }
+
+ private static void ParseObjectTypes(string content, Dictionary records)
+ {
+ foreach (Match match in ObjectTypeRegex.Matches(content))
+ {
+ var name = match.Groups[1].Value;
+
+ // Skip if already added
+ if (records.ContainsKey(name))
+ continue;
+
+ var body = match.Groups[2].Value;
+
+ // Skip re-exports (export type { Foo } from "...")
+ if (string.IsNullOrWhiteSpace(body))
+ continue;
+
+ // Skip if it contains function signatures
+ if (body.Contains("=>") || body.Contains("function"))
+ continue;
+
+ var properties = new List();
+ foreach (Match propMatch in PropertyRegex.Matches(body))
+ {
+ var propName = propMatch.Groups[1].Value;
+ var isOptional = propMatch.Groups[2].Value == "?";
+ var tsType = propMatch.Groups[3].Value.Trim();
+
+ // Skip complex inline types
+ if (tsType.Contains("{"))
+ continue;
+
+ var csType = MapToCSharpType(tsType, isOptional);
+
+ // Skip properties with unknown complex types (like RulesyncSkill)
+ var elementType = csType.Replace("IReadOnlyList<", "").Replace(">", "").Replace("?", "");
+ if (!IsKnownType(elementType) && elementType is not "Feature" and not "ToolTarget")
+ continue;
+
+ var defaultValue = GetDefaultValue(propName, csType);
+
+ properties.Add(new PropertyDefinition(
+ propName,
+ ToPascalCase(propName),
+ tsType,
+ csType,
+ isOptional,
+ defaultValue));
+ }
+
+ if (properties.Count > 0)
+ {
+ records[name] = new RecordDefinition(name, properties.ToImmutableArray());
+ }
+ }
+ }
+
+ private static string GenerateEnumSource(EnumDefinition enumDef)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("// ");
+ sb.AppendLine("#nullable enable");
+ sb.AppendLine();
+ sb.AppendLine("namespace RuleSync.Sdk.Models;");
+ sb.AppendLine();
+ sb.AppendLine($"/// {enumDef.Name} enum.");
+ sb.AppendLine($"public enum {enumDef.Name}");
+ sb.AppendLine("{");
+
+ for (int i = 0; i < enumDef.Values.Length; i++)
+ {
+ var value = enumDef.Values[i];
+ sb.AppendLine($" /// {value.TsValue}");
+ sb.Append($" {value.CsName}");
+ if (i < enumDef.Values.Length - 1)
+ sb.AppendLine(",");
+ else
+ sb.AppendLine();
+ }
+
+ sb.AppendLine("}");
+ return sb.ToString();
+ }
+
+ private static string GenerateRecordSource(RecordDefinition recordDef, SourceProductionContext context)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("// ");
+ sb.AppendLine("#nullable enable");
+ sb.AppendLine();
+ sb.AppendLine("using System.Collections.Generic;");
+ sb.AppendLine("using System.Text.Json.Serialization;");
+ sb.AppendLine();
+ sb.AppendLine("namespace RuleSync.Sdk.Models;");
+ sb.AppendLine();
+ sb.AppendLine($"/// {recordDef.Name} options.");
+ sb.AppendLine($"public sealed class {recordDef.Name}");
+ sb.AppendLine("{");
+
+ foreach (var prop in recordDef.Properties)
+ {
+ if (!string.IsNullOrEmpty(prop.DefaultValue))
+ {
+ sb.AppendLine($" /// {prop.Name}. Default: {prop.DefaultValue}");
+ }
+ else
+ {
+ sb.AppendLine($" /// {prop.Name}.");
+ }
+
+ // Add JsonPropertyName for camelCase JSON
+ sb.AppendLine($" [JsonPropertyName(\"{prop.Name}\")]");
+
+ if (prop.IsOptional && string.IsNullOrEmpty(prop.DefaultValue))
+ {
+ sb.AppendLine($" public {prop.CsType} {prop.CsName} {{ get; init; }}");
+ }
+ else if (!string.IsNullOrEmpty(prop.DefaultValue))
+ {
+ sb.AppendLine($" public {prop.CsType} {prop.CsName} {{ get; init; }} = {prop.DefaultValue};");
+ }
+ else if (prop.CsType.StartsWith("IReadOnlyList<") && prop.CsType.EndsWith(">"))
+ {
+ // Initialize collections with empty array to avoid null warnings
+ // Extract element type from "IReadOnlyList"
+ // Start after "IReadOnlyList<" (14 chars), take everything before last ">"
+ var elementType = prop.CsType.Substring(14, prop.CsType.Length - 15);
+ if (string.IsNullOrEmpty(elementType))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ new DiagnosticDescriptor(
+ id: "RSG001",
+ title: "Cannot determine element type",
+ messageFormat: "Cannot determine element type from '{0}' for property '{1}'",
+ category: "SourceGenerator",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true),
+ Location.None,
+ prop.CsType,
+ prop.Name));
+ continue;
+ }
+ sb.AppendLine($" public {prop.CsType} {prop.CsName} {{ get; init; }} = System.Array.Empty<{elementType}>();");
+ }
+ else
+ {
+ sb.AppendLine($" public {prop.CsType} {prop.CsName} {{ get; init; }}");
+ }
+
+ sb.AppendLine();
+ }
+
+ sb.AppendLine("}");
+ return sb.ToString();
+ }
+
+ private static string MapToCSharpType(string tsType, bool isOptional)
+ {
+ var baseType = tsType.Replace(" | undefined", "").Trim();
+ var nullable = isOptional || tsType.Contains("undefined");
+
+ // Handle arrays
+ if (baseType.EndsWith("[]"))
+ {
+ var elementType = baseType.Substring(0, baseType.Length - 2);
+ var mappedElement = MapPrimitiveType(elementType);
+ return nullable
+ ? $"IReadOnlyList<{mappedElement}>?"
+ : $"IReadOnlyList<{mappedElement}>";
+ }
+
+ var mapped = MapPrimitiveType(baseType);
+
+ // For value types, add ? for nullable; for reference types, just the ? suffix
+ if (nullable)
+ {
+ if (mapped == "bool" || mapped == "int")
+ return $"{mapped}?";
+ return $"{mapped}?";
+ }
+
+ return mapped;
+ }
+
+ private static string MapPrimitiveType(string tsType)
+ {
+ switch (tsType.ToLowerInvariant())
+ {
+ case "string":
+ return "string";
+ case "number":
+ return "int";
+ case "boolean":
+ return "bool";
+ case "feature":
+ return "Feature";
+ case "tooltarget":
+ return "ToolTarget";
+ default:
+ return ToPascalCase(tsType);
+ }
+ }
+
+ private static string? GetDefaultValue(string propName, string csType)
+ {
+ switch (propName)
+ {
+ case "silent":
+ return "true";
+ case "verbose":
+ case "delete":
+ case "global":
+ case "dryRun":
+ case "check":
+ case "simulateCommands":
+ case "simulateSubagents":
+ case "simulateSkills":
+ return "false";
+ default:
+ return null;
+ }
+ }
+
+ private static readonly Dictionary ToolNameMappings = new(StringComparer.OrdinalIgnoreCase)
+ {
+ // Map kebab-case names to beautiful PascalCase
+ ["agentsmd"] = "AgentsMd",
+ ["agentsskills"] = "AgentsSkills",
+ ["antigravity"] = "Antigravity",
+ ["augmentcode"] = "AugmentCode",
+ ["augmentcode-legacy"] = "AugmentCodeLegacy",
+ ["claudecode"] = "ClaudeCode",
+ ["claudecode-legacy"] = "ClaudeCodeLegacy",
+ ["cline"] = "Cline",
+ ["codexcli"] = "CodexCli",
+ ["copilot"] = "Copilot",
+ ["cursor"] = "Cursor",
+ ["factorydroid"] = "FactoryDroid",
+ ["geminicli"] = "GeminiCli",
+ ["goose"] = "Goose",
+ ["junie"] = "Junie",
+ ["kilo"] = "Kilo",
+ ["kiro"] = "Kiro",
+ ["opencode"] = "OpenCode",
+ ["qwencode"] = "QwenCode",
+ ["replit"] = "Replit",
+ ["roo"] = "Roo",
+ ["warp"] = "Warp",
+ ["windsurf"] = "Windsurf",
+ ["zed"] = "Zed"
+ };
+
+ private static string ToPascalCase(string input)
+ {
+ if (string.IsNullOrEmpty(input)) return input;
+
+ // Handle special characters
+ if (input == "*")
+ return "Wildcard";
+
+ // Check for known tool name mappings first
+ if (ToolNameMappings.TryGetValue(input, out var mappedName))
+ return mappedName;
+
+ // Handle hyphenated, snake_case, or camelCase
+ var parts = input.Split('-', '_');
+ var result = string.Concat(parts.Select(p =>
+ string.IsNullOrEmpty(p) ? "" : char.ToUpperInvariant(p[0]) + p.Substring(1)));
+
+ // Handle camelCase (first char already uppercase after split)
+ if (char.IsLower(result[0]))
+ result = char.ToUpperInvariant(result[0]) + result.Substring(1);
+
+ return result;
+ }
+
+ // Class definitions for parsing (using classes instead of records for netstandard2.0 compatibility)
+ private sealed class EnumDefinition
+ {
+ public EnumDefinition(string name, ImmutableArray values)
+ {
+ Name = name;
+ Values = values;
+ }
+
+ public string Name { get; }
+ public ImmutableArray Values { get; }
+ }
+
+ private sealed class EnumValue
+ {
+ public EnumValue(string tsValue, string csName)
+ {
+ TsValue = tsValue;
+ CsName = csName;
+ }
+
+ public string TsValue { get; }
+ public string CsName { get; }
+ }
+
+ private sealed class RecordDefinition
+ {
+ public RecordDefinition(string name, ImmutableArray properties)
+ {
+ Name = name;
+ Properties = properties;
+ }
+
+ public string Name { get; }
+ public ImmutableArray Properties { get; }
+ }
+
+ private sealed class PropertyDefinition
+ {
+ public PropertyDefinition(
+ string name,
+ string csName,
+ string tsType,
+ string csType,
+ bool isOptional,
+ string? defaultValue)
+ {
+ Name = name;
+ CsName = csName;
+ TsType = tsType;
+ CsType = csType;
+ IsOptional = isOptional;
+ DefaultValue = defaultValue;
+ }
+
+ public string Name { get; }
+ public string CsName { get; }
+ public string TsType { get; }
+ public string CsType { get; }
+ public bool IsOptional { get; }
+ public string? DefaultValue { get; }
+ }
+}
diff --git a/sdk/dotnet/src/RuleSync.Sdk.DotNet/Polyfills/ProcessExtensions.cs b/sdk/dotnet/src/RuleSync.Sdk.DotNet/Polyfills/ProcessExtensions.cs
new file mode 100644
index 000000000..f5c96e63d
--- /dev/null
+++ b/sdk/dotnet/src/RuleSync.Sdk.DotNet/Polyfills/ProcessExtensions.cs
@@ -0,0 +1,53 @@
+#nullable enable
+
+#if NETSTANDARD2_1
+using System;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace RuleSync.Sdk.Polyfills;
+
+/// Polyfill for WaitForExitAsync on .NET Standard 2.1.
+internal static class ProcessExtensions
+{
+ public static Task WaitForExitAsync(this Process process, CancellationToken cancellationToken = default)
+ {
+ var tcs = new TaskCompletionSource