Skip to content

feat(editor): add GitHub-based skill sync window with safe incremental mirroring#845

Open
BaronCyrus wants to merge 6 commits intoCoplayDev:betafrom
BaronCyrus:beta
Open

feat(editor): add GitHub-based skill sync window with safe incremental mirroring#845
BaronCyrus wants to merge 6 commits intoCoplayDev:betafrom
BaronCyrus:beta

Conversation

@BaronCyrus
Copy link

@BaronCyrus BaronCyrus commented Feb 28, 2026

Description

This PR adds a new Unity EditorWindow (McpForUnitySkillInstaller) that syncs the unity-mcp skill directly from a GitHub repository without cloning.

The installer:

  • Reads the remote repository tree through the GitHub Tree API.
  • Builds an incremental sync plan (Added / Updated / Deleted) by comparing remote blob SHAs with local git blob SHA-1 values.
  • Downloads only changed files from raw.githubusercontent.com.
  • Applies mirror updates (including deletions), then validates synced files by hash.
  • Persists the last synced commit in EditorPrefs.
  • Supports branch selection, CLI type selection (codex / claude), configurable install path, and detailed log output.
  • Aborts sync when the GitHub tree response is truncated to prevent accidental deletion from incomplete snapshots.

A corresponding .meta file was also added for the new script asset.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)

Changes Made

  • Added McpForUnitySkillInstaller EditorWindow implementation.
  • Added .meta for McpForUnitySkillInstaller.cs.
  • Implemented GitHub API tree fetch (recursive=1) for remote snapshot creation.
  • Implemented local/remote git blob SHA-1 comparison for incremental planning.
  • Implemented selective download for added/updated files and cleanup for deleted files.
  • Added post-sync hash validation for all remote-tracked files.
  • Added EditorPrefs persistence for last synced commit (scoped keying).
  • Added UI controls for branch, CLI type, install path, and operational logs.
  • Added safety guard to abort sync on truncated GitHub tree responses.

Testing/Screenshots/Recordings

Manual validation performed:

  • Initial sync into an empty install directory.
  • Re-sync with no upstream changes (no-op behavior).
  • Sync after upstream file changes (added/updated/deleted reflected correctly).
  • Safety check: verified sync aborts when tree response is truncated.
  • Screenshots/recordings
record.2026-02-28.16.02.05.mov
screenshot2026-02-28 16 05 07

Related Issues

Relates to review feedback about handling truncated GitHub tree responses safely.

Additional Notes

  • The sync flow is clone-free and network-efficient (downloads changed files only).
  • Hash validation ensures file integrity after apply.

Summary by Sourcery

Add a Unity Editor window to sync the unity-mcp skill from a GitHub repo via incremental, hash-validated mirroring without cloning.

New Features:

  • Introduce the McpForUnitySkillInstaller EditorWindow to configure and run GitHub-based sync of the unity-mcp skill into a local install directory.
  • Add support for selecting branch and CLI type, configuring install path, and viewing detailed sync logs within the Unity editor.

Bug Fixes:

  • Prevent unsafe deletions by aborting sync when the GitHub Tree API response is truncated, avoiding acting on incomplete remote snapshots.

Enhancements:

  • Implement incremental sync planning using Git blob SHA-1 comparison to minimize downloads and file writes.
  • Add post-sync hash validation of all tracked files to ensure local skill contents match the remote repository state.
  • Persist last-synced commit per repo/branch/subdirectory scope to avoid unnecessary work when there are no upstream changes.
  • Automatically clean up deleted files and empty directories to keep the install directory mirrored to the remote skill tree.

Summary by CodeRabbit

  • New Features
    • Adds an "Install Skills" workflow in the Unity Editor to sync MCP skills from a configurable repo and branch.
    • UI to start sync, shows progress and sanitized real-time logs, and persists user preferences.
    • Mirrors remote files locally with hash verification and reports added/updated/deleted counts or errors.
    • Client configurators expose skill support and default install paths for supported CLIs.

Add an EditorWindow (McpForUnitySkillInstaller) that syncs the unity-mcp skill from a GitHub repository without cloning. The tool reads the repo tree via the GitHub API, computes git blob SHA-1s to build an incremental sync plan (added/updated/deleted), downloads raw files for changed items, validates hashes, and writes a last-synced commit to EditorPrefs. UI supports branch and CLI (codex/claude) selection, configurable install path, logging, and safety checks (aborts on truncated trees). Also add the corresponding .meta file.
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 28, 2026

Reviewer's Guide

Adds a new Unity EditorWindow that incrementally syncs the unity-mcp skill from a GitHub repo via the GitHub Tree API, performs SHA-based planning and verification, and persists per-repo/branch sync state in EditorPrefs with a safety guard for truncated API responses.

Sequence diagram for GitHub-based incremental skill sync

sequenceDiagram
    actor Developer
    participant UnityEditor as UnityEditor
    participant Installer as McpForUnitySkillInstaller
    participant GitHubAPI as GitHub_API
    participant RawContent as Raw_GitHub_Content
    participant FS as FileSystem
    participant Prefs as EditorPrefs

    Developer->>UnityEditor: Open Window/MCP For Unity/Install(Sync) MCP Skill
    UnityEditor->>Installer: Create window instance
    Installer->>Prefs: Load RepoUrl, Branch, Cli, InstallDir

    Developer->>Installer: Click Sync Latest
    Installer->>Installer: ExecuteWithGuard(action)
    Installer->>Installer: RunSyncLatest()

    Installer->>Installer: TryParseGitHubRepository(_repoUrl)
    Installer->>GitHubAPI: GET /repos/{owner}/{repo}/git/trees/{branch}?recursive=1
    GitHubAPI-->>Installer: GitHubTreeResponse
    Installer->>Installer: Check truncated flag
    Installer->>Installer: Build RemoteSnapshot(commitSha, files)

    Installer->>FS: ListFiles(installPath)
    FS-->>Installer: local files map
    Installer->>Installer: BuildPlan(remoteFiles, localFiles)

    Installer->>RawContent: DownloadBytes() for Added/Updated files
    RawContent-->>Installer: file bytes
    Installer->>FS: WriteAllBytes(targetFile)
    loop For each Deleted file
        Installer->>FS: Delete(targetFile)
    end
    Installer->>FS: RemoveEmptyDirectories(installRoot)

    loop Validate each remote file
        Installer->>FS: ReadAllBytes(localFile)
        FS-->>Installer: bytes
        Installer->>Installer: ComputeGitBlobSha1(bytes)
    end

    Installer->>Prefs: SetString(LastSyncedCommitKeyScope, commitSha)
    Installer-->>Developer: Display log summary in window
Loading

Class diagram for McpForUnitySkillInstaller and related types

classDiagram
    class McpForUnitySkillInstaller {
        -string RepoUrlKey
        -string BranchKey
        -string CliKey
        -string InstallDirKey
        -string LastSyncedCommitKey
        -string FixedSkillSubdir
        -string CodexCli
        -string ClaudeCli
        -string[] BranchOptions
        -string[] CliOptions
        -string _repoUrl
        -string _targetBranch
        -string _cliType
        -string _installDir
        -Vector2 _scroll
        -bool _isRunning
        -ConcurrentQueue~Action~ _mainThreadActions
        -ConcurrentQueue~string~ _pendingLogs
        -StringBuilder _logBuilder
        +OpenWindow()
        +OnEnable()
        +OnDisable()
        +OnGUI()
        -OnEditorUpdate()
        -RunSyncLatest()
        -ExecuteWithGuard(action)
        -GetLastSyncedCommitKey() string
        -FetchRemoteSnapshot(repoInfo, branch, subdir) RemoteSnapshot
        -ApplyPlan(repoInfo, commitSha, remoteSubdir, targetRoot, plan)
        -ValidateFileHashes(installRoot, remoteFiles)
        -GetInstallPath() string
        -TryApplyCliDefaultInstallPath(previousCli, currentCli)
        -GetDefaultInstallDir(userHome, cliType) string
        -IsClaudeCli(cliType) bool
        -PathsEqual(left, right) bool
        -ExpandPath(path) string
        -EnqueueMainThreadAction(action)
        -ExecuteMainThreadActions()
        -AppendLine(line)
        -AppendLineImmediate(line)
        -FlushPendingLogs() bool
        -SanitizeLogLine(line) string
        -LogPlanDetails(plan)
        -AppendSummary(plan, commitChanged)
        -ShortCommit(commit) string
        -ShortHash(hash) string
        -CreateGitHubClient() HttpClient
        -BuildTreeApiUrl(repoInfo, branch) string
        -BuildRawFileUrl(repoInfo, commitSha, remoteFilePath) string
        -DownloadString(client, url) string
        -DownloadBytes(client, url) byte[]
        -NormalizeRemotePath(path) string
        -CombineRemotePath(left, right) string
        -BuildPlan(remoteFiles, localFiles) SyncPlan
        -ComputeGitBlobSha1(filePath) string
        -ComputeGitBlobSha1(bytes) string
        -ListFiles(root) Dictionary~string,string~
        -RemoveEmptyDirectories(root)
        -TryParseGitHubRepository(url) bool
        -TryParseOwnerAndRepo(path) bool
    }

    class GitHubRepoInfo {
        +string Owner
        +string Repo
        +GitHubRepoInfo(owner, repo)
    }

    class RemoteSnapshot {
        +string CommitSha
        +string SubdirPath
        +Dictionary~string,string~ Files
        +RemoteSnapshot(commitSha, subdirPath, files)
    }

    class GitHubTreeResponse {
        +string sha
        +GitHubTreeEntry[] tree
        +bool truncated
    }

    class GitHubTreeEntry {
        +string path
        +string type
        +string sha
    }

    class SyncPlan {
        +List~string~ Added
        +List~string~ Updated
        +List~string~ Deleted
    }

    McpForUnitySkillInstaller *-- GitHubRepoInfo
    McpForUnitySkillInstaller *-- RemoteSnapshot
    McpForUnitySkillInstaller *-- GitHubTreeResponse
    GitHubTreeResponse *-- GitHubTreeEntry
    McpForUnitySkillInstaller *-- SyncPlan
Loading

File-Level Changes

Change Details Files
Introduce McpForUnitySkillInstaller EditorWindow that performs clone-free, incremental GitHub-based skill mirroring with logging UI.
  • Create an EditorWindow with configurable repo URL, branch (beta/main), CLI type (codex/claude), and install directory, persisting these via EditorPrefs.
  • Implement async-guarded sync entrypoint that parses the GitHub repo URL, fetches a recursive tree snapshot for a fixed unity-mcp-skill subdir, and computes a sync plan (Added/Updated/Deleted) by comparing remote blob SHAs with locally computed git-blob SHA-1 hashes.
  • Download added/updated files directly from raw.githubusercontent.com at a specific commit, delete files that are no longer present upstream, and prune now-empty directories.
  • After applying the plan, validate each tracked file by recomputing git-blob SHA-1 and comparing to the remote tree; on success, update a scoped last-synced-commit key stored in EditorPrefs using a SHA-256 hash of repo/branch/subdir for namespacing.
  • Provide a scrollable log output area with timestamped, ANSI-stripped log lines, including detailed per-file operations and a human-readable summary of the sync outcome, plus a Clear Log button and main-thread action queue for safe EditorPrefs updates.
MCPForUnity/Editor/Setup/McpForUnitySkillInstaller.cs
Add GitHub integration helpers for tree and raw file access with safety checks.
  • Implement GitHub URL parsing for HTTPS and SSH formats into an owner/repo pair, with normalization of .git suffix.
  • Add helpers to call the GitHub Tree API (git/trees/{branch}?recursive=1) and raw.githubusercontent.com, with HttpClient configuration (User-Agent, Accept header, timeout) and detailed error messages on non-success status codes.
  • Filter and normalize tree entries into a relative-path → blob-SHA map under a specified subdirectory, requiring a non-truncated response and a non-empty file set and commit SHA; abort sync if the tree is truncated.
  • Normalize and compose remote paths consistently (forward slashes, trimmed segments) when building URLs and doing subdir filtering.
MCPForUnity/Editor/Setup/McpForUnitySkillInstaller.cs
Implement local filesystem utilities for planning and applying sync using git-style blob hashes.
  • Traverse the install directory to build a relative-path → absolute-path map, and compute git blob SHA-1 hashes by hashing blob {length}\0 headers plus file bytes.
  • Create a SyncPlan data structure and builder that classifies files as Added, Updated, or Deleted, sorts each list deterministically, and logs per-file deltas (+/~/-).
  • Apply the sync plan by writing downloaded bytes to disk (creating directories as needed), deleting obsolete files, and removing any empty directories bottom-up.
MCPForUnity/Editor/Setup/McpForUnitySkillInstaller.cs
Add path, CLI, and commit-scoping helpers plus log sanitization and threading utilities.
  • Provide helpers to expand ~ to the user home directory, normalize and compare paths robustly, and derive default install directories under ~/.codex or ~/.claude depending on selected CLI, auto-updating the install path when the CLI changes if it was still at the old default.
  • Compute a unique EditorPrefs key for the last-synced commit by hashing repo URL, branch, and normalized subdir with SHA-256, preventing key collisions across configurations.
  • Implement a main-thread action queue executed in OnEditorUpdate to safely interact with Unity APIs from background tasks, and a guarded async execution wrapper that prevents concurrent sync runs and surfaces errors as log entries.
  • Strip ANSI escape sequences and non-printable characters from logs before recording them, and maintain a scroll position that automatically scrolls to the bottom when new logs arrive.
MCPForUnity/Editor/Setup/McpForUnitySkillInstaller.cs
Add Unity .meta file for the new Editor script asset.
  • Introduce a .meta file alongside the new McpForUnitySkillInstaller.cs so Unity can import and reference the Editor script correctly.
MCPForUnity/Editor/Setup/McpForUnitySkillInstaller.cs.meta

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 28, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new Unity Editor skill installer and a Git-backed SkillSyncService; exposes configurator APIs for client skill support and wires an "Install Skills" UI button that syncs a GitHub subtree into a local install path with plan computation, file apply, and verification.

Changes

Cohort / File(s) Summary
Installer UI
MCPForUnity/Editor/Setup/McpForUnitySkillInstaller.cs
New Unity Editor window for configuring and running skill syncs: persistent prefs, UI logs, queueing, path normalization, validation, and safe main-thread updates.
Sync Engine
MCPForUnity/Editor/Setup/SkillSyncService.cs
New service to parse GitHub URLs, fetch repo tree, build add/update/delete plan, download raw blobs, apply changes, verify Git blob SHAs, and return structured SyncResult via async callbacks.
Metafiles
MCPForUnity/Editor/Setup/McpForUnitySkillInstaller.cs.meta, MCPForUnity/Editor/Setup/SkillSyncService.cs.meta
New Unity .meta files for added C# assets.
Client Configurators & API
MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs, MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs, MCPForUnity/Editor/Clients/Configurators/ClaudeCodeConfigurator.cs, MCPForUnity/Editor/Clients/Configurators/ClaudeDesktopConfigurator.cs, MCPForUnity/Editor/Clients/Configurators/CodexConfigurator.cs
Added SupportsSkills property and GetSkillInstallPath() to configurator API/base; implemented in Claude/Codex configurators to provide per-client default skill install paths.
Client UI wiring
MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs, MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.uxml
Added "Install Skills" button, visibility controlled by selected client's SupportsSkills, and handler that invokes SkillSyncService.SyncAsync and surfaces results/errors.

Sequence Diagram

sequenceDiagram
    participant Editor as Unity Editor
    participant UI as Installer UI
    participant GitHub as GitHub API
    participant FS as Local FileSystem
    participant Prefs as EditorPrefs

    Editor->>UI: OpenWindow()
    UI->>Prefs: Load saved config (repo, branch, cli, dir)
    UI->>GitHub: Request git/trees?recursive=1 (repo, branch)
    GitHub-->>UI: Return tree entries + SHAs
    UI->>FS: Read local install dir and marker files
    UI->>UI: Compute SyncPlan (Added/Updated/Deleted)
    UI->>Editor: Display plan / start sync
    UI->>GitHub: Fetch raw blobs for Added/Updated files
    GitHub-->>UI: Return file contents
    UI->>FS: Write/update files, delete removed files, cleanup dirs
    FS-->>UI: Confirm writes/deletes
    UI->>UI: Verify Git blob SHA1s match remote SHAs
    UI->>Prefs: Persist last-run metadata & settings
    UI-->>Editor: Log results and return SyncResult
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

codex

Suggested reviewers

  • msanatan
  • dsarno

Poem

🐰 I hop through trees of SHA and name,
Fetch blobs by branch and stitch each frame,
I write, I trim, I mark the root,
Save prefs, log hops, then tap my foot,
Skill synced! I nibble code with fame. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: a GitHub-based skill sync editor window with safe incremental mirroring, which matches the primary objective of the PR.
Description check ✅ Passed The PR description comprehensively covers all required sections: detailed description of changes, type of change selection, specific changes made, testing validation, related issues, and additional notes. It exceeds template requirements with thorough documentation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • The window class is quite large and mixes UI, GitHub API, planning, and filesystem concerns; consider extracting the GitHub client/snapshot logic and the sync planner/applicator into separate classes to simplify the EditorWindow and make behavior easier to reason about.
  • Each sync run creates and disposes multiple HttpClient instances and uses GetAwaiter().GetResult(); consider reusing a single HttpClient instance and/or switching to async I/O to avoid connection churn and potential thread pool blocking.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The window class is quite large and mixes UI, GitHub API, planning, and filesystem concerns; consider extracting the GitHub client/snapshot logic and the sync planner/applicator into separate classes to simplify the EditorWindow and make behavior easier to reason about.
- Each sync run creates and disposes multiple HttpClient instances and uses `GetAwaiter().GetResult()`; consider reusing a single HttpClient instance and/or switching to async I/O to avoid connection churn and potential thread pool blocking.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 49901a3b9a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@MCPForUnity/Editor/Setup/McpForUnitySkillInstaller.cs`:
- Line 102: The installer currently accepts any path via the _installDir field
and later deletes files in that directory; add safeguards in
McpForUnitySkillInstaller around path selection and deletion: validate
_installDir to ensure it is within the Unity project (e.g., under
Application.dataPath) or contains a marker file (like ".mcp_managed") before
performing any destructive sync in the method that performs deletions (the
sync/install routine that iterates files at lines ~435-475), present a blocking
EditorUtility.DisplayDialog confirmation requiring the user to type the
directory name for destructive actions, and refuse to run the deletion code if
the ownership check or validation fails (log an error and abort) so only
managed, explicitly confirmed folders are modified; also apply the same
validation to any other UI setters that assign _installDir (the TextField and
any code paths around lines ~163-175).
- Around line 303-323: The code currently accepts remotePath from
NormalizeRemotePath and builds relativePath which is later combined with install
root; add strict path traversal containment checks before adding to remoteFiles
and before any Path.Combine I/O: ensure relativePath is not rooted
(Path.IsPathRooted), split the path on Path.DirectorySeparatorChar and
Path.AltDirectorySeparatorChar and reject if any segment equals ".." or empty
segments that imply traversal, and after resolving the final path use
Path.GetFullPath(combinedPath) and verify it starts with the normalized install
root full path (installRootFullPath + Path.DirectorySeparatorChar) to guarantee
containment; if any check fails, skip the entry and log or ignore it.
- Around line 331-338: The code is using treeResponse.sha (the tree object SHA)
as the commit SHA; instead, retrieve the branch head commit SHA before
requesting the tree and propagate that commit SHA into RemoteSnapshot and
BuildRawFileUrl. Concretely: in McpForUnitySkillInstaller where you call the
Tree API, first fetch the branch reference (the branch head commit SHA from the
branch ref/commit object) and store it (e.g., branchCommitSha), then replace
uses of treeResponse.sha with that branchCommitSha when constructing the
RemoteSnapshot and when calling BuildRawFileUrl so raw.githubusercontent.com
receives a valid commit-ish identifier.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 78bf4a0 and 49901a3.

📒 Files selected for processing (2)
  • MCPForUnity/Editor/Setup/McpForUnitySkillInstaller.cs
  • MCPForUnity/Editor/Setup/McpForUnitySkillInstaller.cs.meta

- Resolve branch head commit SHA via GitHub branches API and use it for snapshot/raw download refs
- Reject unsafe remote relative paths and enforce install-root containment for all file IO
- Handle case-only renames on case-insensitive filesystems during sync planning
- Add managed install-root marker guard to avoid destructive deletes in unmanaged directories
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@MCPForUnity/Editor/Setup/McpForUnitySkillInstaller.cs`:
- Around line 72-104: OnGUI/RunSyncLatest currently capture mutable fields
(_repoUrl, _targetBranch, _installDir) into a background job while the UI
remains editable (_isRunning gating is incomplete); fix by snapshotting those
values into local readonly variables (e.g., var repoUrl = _repoUrl; var
targetBranch = _targetBranch; var installDir = _installDir;) immediately before
queuing the background action in RunSyncLatest and use those locals inside the
queued lambda instead of the fields; ensure the check/setting of _isRunning
still happens on the main thread and that TryApplyCliDefaultInstallPath and
other UI state changes are not invoked from the background task.
- Around line 157-162: Add explicit validation of the install path returned by
GetInstallPath before any filesystem calls: check that installPath is not
null/empty and is a valid path (e.g., not whitespace and does not contain
invalid path chars) and if invalid throw or log a clear error and return early
instead of calling Directory.CreateDirectory(installPath); update both places
where this pattern appears (the block using GetInstallPath around the
CreateDirectory call and the similar code at the other occurrence) to fail fast
with a descriptive message referencing GetInstallPath/installPath.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 49901a3 and a031b57.

📒 Files selected for processing (1)
  • MCPForUnity/Editor/Setup/McpForUnitySkillInstaller.cs

- Snapshot repo/branch/installDir before starting background sync task
- Disable config inputs while sync is running to avoid mid-run mutations
- Validate install directory explicitly with clear errors before filesystem operations
- Scope last-synced-commit key derivation to captured repo/branch values
@BaronCyrus
Copy link
Author

Thanks everyone for the reviews. All actionable issues from this round have been fixed and pushed to beta:

  • a031b57:

    • Fixed rooted/path-traversal risks by validating remote relative paths and enforcing install-root containment for all file I/O.
    • Fixed case-only rename behavior on case-insensitive filesystems to prevent add+delete collisions.
    • Stopped using tree SHA as commit SHA; now resolves and uses the branch head commit SHA.
    • Added managed install-root marker protection (.unity-mcp-skill-sync) to avoid destructive deletes in unmanaged directories.
  • 5f833dc:

    • Fixed config race conditions by snapshotting repoUrl/branch/installDir before queuing background sync and using only captured values in the worker.
    • Disabled config inputs while sync is running to prevent mid-run mutations.
    • Added explicit install-path validation with clear fail-fast errors for empty/invalid paths.

Current status:

  • Latest CodeRabbit result reports: “No actionable comments were generated in the recent review.”

Note:

  • I agree with Sourcery’s higher-level suggestions (separating concerns and reusing/async HttpClient), but those are architectural improvements and are better handled in a follow-up PR to keep this fix PR focused and low-risk.

1. Incorporate the Config into a simple one-click in Client Configuration.
2. Simplify the logs to only 1 entry, convenient for LLM checking.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
MCPForUnity/Editor/Setup/SkillSyncService.cs (1)

280-289: Consider noting HttpClient reuse as a follow-up.

HttpClient is designed to be reused rather than created per-request. Creating multiple instances can lead to socket exhaustion under heavy use. Per PR comments, this is acknowledged for a follow-up PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCPForUnity/Editor/Setup/SkillSyncService.cs` around lines 280 - 289,
CreateGitHubClient currently instantiates a new HttpClient per call which can
cause socket exhaustion; change it to return a single reused instance by
introducing a static readonly HttpClient (e.g., _githubClient) initialized once
with the same Timeout, UserAgent and Accept headers and have CreateGitHubClient
return that instance; update any callers to continue using CreateGitHubClient
and add a comment/TODO indicating this reuse is intentional and was moved here
as the follow-up noted in the PR.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@MCPForUnity/Editor/Setup/SkillSyncService.cs`:
- Around line 41-62: The log callback passed into RunSync is invoked from a
background Task.Run thread which can call Unity APIs unsafely; wrap or replace
the passed-in log delegate so all log invocations are marshalled to the main
thread via EditorApplication.delayCall before calling the original callback.
Specifically, before calling RunSync in the Task.Run block, create a thread-safe
wrapper (e.g., mainThreadLog) that captures the original log and invokes
EditorApplication.delayCall += () => originalLog(message) for each call, then
pass that wrapper into RunSync; keep using EditorApplication.delayCall for the
final onComplete and lastSyncedCommit update as already done.
- Around line 436-465: ApplyPlan can incorrectly delete files on
case-insensitive filesystems when a rename only changes case because the code
writes Added entries then deletes Deleted entries; detect case-only renames by
comparing entries from plan.Added and plan.Deleted using
StringComparison.OrdinalIgnoreCase but not exact equality, resolve their target
paths via ResolvePathUnderRoot, and perform the deletion of the old-case file(s)
before writing the new file(s). Update ApplyPlan to (1) build a set of
case-only-deletes (using ResolvePathUnderRoot to get exact file paths) and
delete those files up front, (2) then proceed with downloading/writing for
plan.Added/Updated using DownloadBytes/CombineRemotePath/BuildRawFileUrl as
before, and (3) keep the remaining Deleted processing and call
RemoveEmptyDirectories at the end.

---

Nitpick comments:
In `@MCPForUnity/Editor/Setup/SkillSyncService.cs`:
- Around line 280-289: CreateGitHubClient currently instantiates a new
HttpClient per call which can cause socket exhaustion; change it to return a
single reused instance by introducing a static readonly HttpClient (e.g.,
_githubClient) initialized once with the same Timeout, UserAgent and Accept
headers and have CreateGitHubClient return that instance; update any callers to
continue using CreateGitHubClient and add a comment/TODO indicating this reuse
is intentional and was moved here as the follow-up noted in the PR.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5f833dc and b5d8e2a.

📒 Files selected for processing (10)
  • MCPForUnity/Editor/Clients/Configurators/ClaudeCodeConfigurator.cs
  • MCPForUnity/Editor/Clients/Configurators/ClaudeDesktopConfigurator.cs
  • MCPForUnity/Editor/Clients/Configurators/CodexConfigurator.cs
  • MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs
  • MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
  • MCPForUnity/Editor/Setup/McpForUnitySkillInstaller.cs
  • MCPForUnity/Editor/Setup/SkillSyncService.cs
  • MCPForUnity/Editor/Setup/SkillSyncService.cs.meta
  • MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
  • MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.uxml
🚧 Files skipped from review as they are similar to previous changes (1)
  • MCPForUnity/Editor/Setup/McpForUnitySkillInstaller.cs

Comment on lines +41 to +62
Task.Run(() =>
{
try
{
var result = RunSync(repoUrl, installDir, branch, lastSyncedCommit, log);
EditorApplication.delayCall += () =>
{
if (result.Success && !string.IsNullOrEmpty(result.CommitSha))
{
EditorPrefs.SetString(lastSyncedCommitKey, result.CommitSha);
}
onComplete?.Invoke(result);
};
}
catch (Exception ex)
{
EditorApplication.delayCall += () =>
{
onComplete?.Invoke(new SyncResult { Success = false, Error = ex.Message });
};
}
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Log callback may be invoked from background thread.

The log callback is passed to RunSync which executes on Task.Run. If the caller's log implementation interacts with Unity APIs (e.g., updating UI elements, calling Debug.Log), this will cause threading issues since Unity APIs must be called from the main thread.

Consider marshalling log invocations back to the main thread, or document that the callback must be thread-safe.

🛠️ Proposed fix: Marshal log calls to main thread
-        public static void SyncAsync(string repoUrl, string installDir, string branch, Action<string> log, Action<SyncResult> onComplete)
+        public static void SyncAsync(string repoUrl, string installDir, string branch, Action<string> log, Action<SyncResult> onComplete)
         {
             var lastSyncedCommitKey = GetLastSyncedCommitKey(repoUrl, branch);
             var lastSyncedCommit = EditorPrefs.GetString(lastSyncedCommitKey, string.Empty);
 
+            // Thread-safe log wrapper that marshals to main thread
+            Action<string> threadSafeLog = message =>
+            {
+                if (log != null)
+                {
+                    EditorApplication.delayCall += () => log(message);
+                }
+            };
+
             Task.Run(() =>
             {
                 try
                 {
-                    var result = RunSync(repoUrl, installDir, branch, lastSyncedCommit, log);
+                    var result = RunSync(repoUrl, installDir, branch, lastSyncedCommit, threadSafeLog);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Task.Run(() =>
{
try
{
var result = RunSync(repoUrl, installDir, branch, lastSyncedCommit, log);
EditorApplication.delayCall += () =>
{
if (result.Success && !string.IsNullOrEmpty(result.CommitSha))
{
EditorPrefs.SetString(lastSyncedCommitKey, result.CommitSha);
}
onComplete?.Invoke(result);
};
}
catch (Exception ex)
{
EditorApplication.delayCall += () =>
{
onComplete?.Invoke(new SyncResult { Success = false, Error = ex.Message });
};
}
});
public static void SyncAsync(string repoUrl, string installDir, string branch, Action<string> log, Action<SyncResult> onComplete)
{
var lastSyncedCommitKey = GetLastSyncedCommitKey(repoUrl, branch);
var lastSyncedCommit = EditorPrefs.GetString(lastSyncedCommitKey, string.Empty);
// Thread-safe log wrapper that marshals to main thread
Action<string> threadSafeLog = message =>
{
if (log != null)
{
EditorApplication.delayCall += () => log(message);
}
};
Task.Run(() =>
{
try
{
var result = RunSync(repoUrl, installDir, branch, lastSyncedCommit, threadSafeLog);
EditorApplication.delayCall += () =>
{
if (result.Success && !string.IsNullOrEmpty(result.CommitSha))
{
EditorPrefs.SetString(lastSyncedCommitKey, result.CommitSha);
}
onComplete?.Invoke(result);
};
}
catch (Exception ex)
{
EditorApplication.delayCall += () =>
{
onComplete?.Invoke(new SyncResult { Success = false, Error = ex.Message });
};
}
});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCPForUnity/Editor/Setup/SkillSyncService.cs` around lines 41 - 62, The log
callback passed into RunSync is invoked from a background Task.Run thread which
can call Unity APIs unsafely; wrap or replace the passed-in log delegate so all
log invocations are marshalled to the main thread via
EditorApplication.delayCall before calling the original callback. Specifically,
before calling RunSync in the Task.Run block, create a thread-safe wrapper
(e.g., mainThreadLog) that captures the original log and invokes
EditorApplication.delayCall += () => originalLog(message) for each call, then
pass that wrapper into RunSync; keep using EditorApplication.delayCall for the
final onComplete and lastSyncedCommit update as already done.

Comment on lines +436 to +465
private static void ApplyPlan(GitHubRepoInfo repoInfo, string commitSha, string remoteSubdir, string targetRoot, SyncPlan plan, StringComparison pathComparison, Action<string> log)
{
using var client = CreateGitHubClient();
foreach (var relativePath in plan.Added.Concat(plan.Updated))
{
var remoteFilePath = CombineRemotePath(remoteSubdir, relativePath);
var downloadUrl = BuildRawFileUrl(repoInfo, commitSha, remoteFilePath);
var targetFile = ResolvePathUnderRoot(targetRoot, relativePath, pathComparison);
var targetDirectory = Path.GetDirectoryName(targetFile);
if (!string.IsNullOrEmpty(targetDirectory))
{
Directory.CreateDirectory(targetDirectory);
}

log?.Invoke($"Download: {relativePath}");
var bytes = DownloadBytes(client, downloadUrl);
File.WriteAllBytes(targetFile, bytes);
}

foreach (var relativePath in plan.Deleted)
{
var targetFile = ResolvePathUnderRoot(targetRoot, relativePath, pathComparison);
if (File.Exists(targetFile))
{
File.Delete(targetFile);
}
}

RemoveEmptyDirectories(targetRoot);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Case-only rename may delete the file on case-insensitive filesystems.

When a file is renamed with only case changes (e.g., File.txtfile.txt), the plan will have the new name in Added and the old name in Deleted. The current operation order (add/update first, then delete) will:

  1. Write file.txt
  2. Delete File.txt — which on case-insensitive FS deletes the file just written

This results in data loss.

🐛 Proposed fix: Handle case-only renames explicitly
         private static void ApplyPlan(GitHubRepoInfo repoInfo, string commitSha, string remoteSubdir, string targetRoot, SyncPlan plan, StringComparison pathComparison, Action<string> log)
         {
             using var client = CreateGitHubClient();
+
+            // On case-insensitive FS, detect case-only renames to avoid delete clobbering add
+            var caseOnlyRenames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+            if (pathComparison == StringComparison.OrdinalIgnoreCase)
+            {
+                var addedLower = new HashSet<string>(plan.Added, StringComparer.OrdinalIgnoreCase);
+                foreach (var deleted in plan.Deleted)
+                {
+                    if (addedLower.Contains(deleted))
+                    {
+                        caseOnlyRenames.Add(deleted);
+                    }
+                }
+            }
+
             foreach (var relativePath in plan.Added.Concat(plan.Updated))
             {
                 var remoteFilePath = CombineRemotePath(remoteSubdir, relativePath);
                 var downloadUrl = BuildRawFileUrl(repoInfo, commitSha, remoteFilePath);
                 var targetFile = ResolvePathUnderRoot(targetRoot, relativePath, pathComparison);
                 var targetDirectory = Path.GetDirectoryName(targetFile);
                 if (!string.IsNullOrEmpty(targetDirectory))
                 {
                     Directory.CreateDirectory(targetDirectory);
                 }
 
                 log?.Invoke($"Download: {relativePath}");
                 var bytes = DownloadBytes(client, downloadUrl);
                 File.WriteAllBytes(targetFile, bytes);
             }
 
             foreach (var relativePath in plan.Deleted)
             {
+                // Skip deletion if this is a case-only rename (file already overwritten)
+                if (caseOnlyRenames.Contains(relativePath))
+                {
+                    continue;
+                }
+
                 var targetFile = ResolvePathUnderRoot(targetRoot, relativePath, pathComparison);
                 if (File.Exists(targetFile))
                 {
                     File.Delete(targetFile);
                 }
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCPForUnity/Editor/Setup/SkillSyncService.cs` around lines 436 - 465,
ApplyPlan can incorrectly delete files on case-insensitive filesystems when a
rename only changes case because the code writes Added entries then deletes
Deleted entries; detect case-only renames by comparing entries from plan.Added
and plan.Deleted using StringComparison.OrdinalIgnoreCase but not exact
equality, resolve their target paths via ResolvePathUnderRoot, and perform the
deletion of the old-case file(s) before writing the new file(s). Update
ApplyPlan to (1) build a set of case-only-deletes (using ResolvePathUnderRoot to
get exact file paths) and delete those files up front, (2) then proceed with
downloading/writing for plan.Added/Updated using
DownloadBytes/CombineRemotePath/BuildRawFileUrl as before, and (3) keep the
remaining Deleted processing and call RemoveEmptyDirectories at the end.

@Scriptwonder
Copy link
Collaborator

Hi, thanks for this PR! I was like "Oh I missed this spot" when I saw this PR, since I am constantly writing and updating the Skills but forgot how the community is having trouble using it directly. Thanks for reminding me.
I also did some change on the UI/Debug messages to make it more clean. It will auto-detect which branch you are on (beta or main), and then install Skills based on that branch, which suffices the original goal you intend to do just with one quick button click.
image
Let me know what you think about the change, I will keep it open for a couple of days. Thanks again!!

@BaronCyrus
Copy link
Author

Hi, thanks for this PR! I was like "Oh I missed this spot" when I saw this PR, since I am constantly writing and updating the Skills but forgot how the community is having trouble using it directly. Thanks for reminding me.
I also did some change on the UI/Debug messages to make it more clean. It will auto-detect which branch you are on (beta or main), and then install Skills based on that branch, which suffices the original goal you intend to do just with one quick button click.
image
Let me know what you think about the change, I will keep it open for a couple of days. Thanks again!!

that's nice thanks for the change

@BaronCyrus
Copy link
Author

Hi, thanks for this PR! I was like "Oh I missed this spot" when I saw this PR, since I am constantly writing and updating the Skills but forgot how the community is having trouble using it directly. Thanks for reminding me.
I also did some change on the UI/Debug messages to make it more clean. It will auto-detect which branch you are on (beta or main), and then install Skills based on that branch, which suffices the original goal you intend to do just with one quick button click.
image
Let me know what you think about the change, I will keep it open for a couple of days. Thanks again!!

which beta version will release this feature ?

@Scriptwonder
Copy link
Collaborator

Once we merge the PR, it should be in the next version (V9.4.8-beta.12 or 13). Will merge it later today!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants