From 2b873a1f4d637dc6545bf4fb4d6f485fb5b69437 Mon Sep 17 00:00:00 2001 From: Myfro9 <36755290+Myfro9@users.noreply.github.com> Date: Mon, 20 Jul 2026 18:28:34 +0800 Subject: [PATCH] sync: WIN10-I9-64G 2026-07-20 --- tools-sync/SKILL.md | 37 +++++---- tools-sync/scripts/sync_repos.ps1 | 126 +++++++++++++++--------------- 2 files changed, 82 insertions(+), 81 deletions(-) diff --git a/tools-sync/SKILL.md b/tools-sync/SKILL.md index 5b1ef12..f152447 100644 --- a/tools-sync/SKILL.md +++ b/tools-sync/SKILL.md @@ -11,9 +11,12 @@ description: 在本机统一同步并配置 Claude Code 与 Codex 的 skills、M 注册表。这个 skill 的职责是让它们保持一致,并且可以在一台新电脑上从零复原整套配置: - **skills 的唯一真源**是 `http://git.rpchannel.com/bowei/skills-win.git`(remote 名 `origin`)。 - `~\.claude\skills` 是主工作副本,`~\.codex\skills` 是同一仓库的第二个检出(镜像)。 + `~\.claude\skills` 是唯一的 git 检出(主副本);`~\.codex\skills` 下的每个 skill + 是指向主副本对应目录的 **junction(目录联接)**——两个工具共享同一份物理文件, + 从机制上杜绝两侧漂移,git 操作只发生在主副本一处。 Codex 没有注册命令——它在会话启动时自动扫描 `~\.codex\skills` 下所有含 `SKILL.md` - 的目录,所以"注册到 Codex" = "目录出现在那里且含 SKILL.md"。 + 的目录(junction 对它透明),所以"注册到 Codex" = "junction 存在且指向的目录含 + SKILL.md",新开的 Codex 会话才会扫到新 skill。 - **机器相关的参数不进 git**。每台电脑的软件安装路径、IP、账号等都记录在 `~\skills_env.md`(用户主目录下,天然在两个仓库检出之外,不会被 git 同步)。 skills 里引用参数时写"见 ~\skills_env.md 的 XXX 条目",不写死路径。 @@ -58,21 +61,23 @@ powershell -File \scripts\sync_repos.ps1 -RepoUrl - **不该入库的文件**:commit 前扫一眼 `git status`,运行时产物(.sqlite、.pckl、 日志、缓存目录)应加进 `.gitignore` 而不是提交。看到就顺手补 `.gitignore`。 -## 阶段 2 — 同步 ~\.codex\skills(镜像检出) +## 阶段 2 — 对齐 ~\.codex\skills(junction 对账) -同一个脚本会接着处理 codex 侧。逻辑和原因: +同一个脚本接着处理 codex 侧。它对主副本的每个顶层 skill 目录做三件事: -- 已是 git 仓库 → `git pull --rebase` 即可。 -- 还不是 git 仓库(首次转换)→ 脚本执行 `git init` + `remote add origin` + - `fetch` + `git reset origin/main`,然后**停下来**列出"本地与仓库不一致"的文件。 - 这一步停是有原因的:codex 目录里可能有比仓库新的本地修改或 codex 独有的 - skill。处理原则——codex 独有且有价值的 skill,先复制到 `~\.claude\skills` - 走阶段 1 入库,再回来;确认没有需要保留的差异后,用 `-Force` 重跑让脚本 - `git checkout -f` 对齐到仓库版本。codex 自带的系统 skills(`.system/`、 - vercel 系列等)已在 `.gitignore` 里,不会被动。 -- 同步完成后校验注册:仓库里每个顶层 skill 目录都应在 `~\.codex\skills` - 出现且含 `SKILL.md`。列出两侧目录做差集,差集应为空(忽略 .gitignore - 中的 codex 专属条目)。提醒用户:Codex 需要开新会话才会扫到新 skills。 +- codex 侧没有同名条目 → 创建 junction(`New-Item -ItemType Junction`, + Windows 上不需要管理员权限,Codex 对 junction 完全透明)。 +- 已是 junction → 跳过(内容天然一致,无需任何拷贝)。 +- 是**真实目录**(挡住了链接)→ 脚本停下来列出这些目录。停的原因:真实目录 + 里可能有 codex 独有的、还没入库的内容。处理原则——有价值的内容先复制进 + `~\.claude\skills` 走阶段 1 入库,确认无需保留后用 `-Force` 重跑,脚本会 + 删掉真实目录并建 junction。 + +脚本还会清理"指向已删除主副本目录"的失效 junction(skill 在仓库里被删除或 +改名后残留的链接)。codex 自带的系统 skills(`.system\`)永远不动。 + +校验注册:主副本每个 skill 目录在 codex 侧都应有同名 junction 且透出 +`SKILL.md`(脚本已内置该检查)。提醒用户:Codex 要开新会话才会扫到新 skills。 ## 阶段 3 — 硬编码审计与 skills_env.md 回填 @@ -97,7 +102,7 @@ powershell -File \scripts\sync_repos.ps1 -RepoUrl 一次审计不必把所有历史 skills 全部重构完;至少保证:新增/本次改动的 skill 零硬编码, 存量 skill 的命中清单写进最终报告,让用户决定分批改。改动之后回到阶段 1 再 -commit + push 一次,并在 codex 侧 pull,保证两边一致。 +commit + push 一次(junction 模型下 codex 侧自动同步,无需额外操作)。 ## 阶段 4 — langgraph 开发环境校验 diff --git a/tools-sync/scripts/sync_repos.ps1 b/tools-sync/scripts/sync_repos.ps1 index a5ee386..2c60e28 100644 --- a/tools-sync/scripts/sync_repos.ps1 +++ b/tools-sync/scripts/sync_repos.ps1 @@ -1,17 +1,20 @@ -# tools-sync: mechanical git sync of the two skills checkouts. -# Claude side (~\.claude\skills) is the primary working copy: commit -> pull --rebase -> push. -# Codex side (~\.codex\skills) is a mirror checkout of the same repo: bootstrap if needed, then pull. -# Judgment calls (conflicts, codex-only skills worth keeping) are left to the caller. +# tools-sync: mechanical sync of skills between Claude Code and Codex. +# Phase 1 — git sync of the primary checkout (~\.claude\skills): commit -> pull --rebase -> push. +# Phase 2 — junction reconciliation: every top-level skill dir in the primary gets a +# directory junction of the same name in ~\.codex\skills, so both tools share +# one physical copy and can never drift. Codex auto-registers any dir there +# containing SKILL.md (new sessions only). +# Judgment calls (rebase conflicts, codex-local dirs worth keeping) are left to the caller. param( [Parameter(Mandatory = $true)][string]$RepoUrl, [string]$ClaudeSkills = "$env:USERPROFILE\.claude\skills", [string]$CodexSkills = "$env:USERPROFILE\.codex\skills", [string]$Branch = "main", - # Bootstrap of the codex checkout stops when local files differ from the repo; - # -Force lets it overwrite them (caller must have rescued codex-only edits first). + # A real (non-junction) dir in the codex side that shadows a primary skill blocks + # linking; -Force deletes it and links (caller must have rescued unique content first). [switch]$Force, - # Report what would happen without committing/pushing/checking out. + # Report what would happen without committing/pushing/linking. [switch]$DryRun ) @@ -32,7 +35,7 @@ function Invoke-Git { function Fail([string]$Msg) { Write-Host "FAIL: $Msg"; exit 1 } # ---------- Phase 1: primary checkout (~\.claude\skills) ---------- -Write-Host "=== [1/2] primary: $ClaudeSkills ===" +Write-Host "=== [1/2] primary git sync: $ClaudeSkills ===" if (-not (Test-Path (Join-Path $ClaudeSkills ".git"))) { Fail "$ClaudeSkills is not a git repo. Clone $RepoUrl there first (new machine: git clone $RepoUrl `"$ClaudeSkills`")." } @@ -65,68 +68,61 @@ if (-not $DryRun) { Write-Host "primary in sync with origin/$Branch" } -# ---------- Phase 2: mirror checkout (~\.codex\skills) ---------- -Write-Host "=== [2/2] mirror: $CodexSkills ===" +# ---------- Phase 2: junction reconciliation (~\.codex\skills) ---------- +Write-Host "=== [2/2] codex junctions: $CodexSkills ===" if (-not (Test-Path $CodexSkills)) { New-Item -ItemType Directory -Force $CodexSkills | Out-Null } -# Bootstrap is needed when there's no repo yet, or a previous bootstrap died before -# origin/main ever arrived (init succeeded, fetch didn't). -$needBootstrap = -not (Test-Path (Join-Path $CodexSkills ".git")) -if (-not $needBootstrap) { - # Upstream config is written as the very last bootstrap step, so its absence - # means a previous bootstrap died partway — redo it (all steps are idempotent). - $r = Invoke-Git $CodexSkills @("config", "--get", "branch.$Branch.merge") - if ($r.Code -ne 0) { $needBootstrap = $true } +# Left over from the older mirror-checkout approach; junctions need no repo here. +if (Test-Path (Join-Path $CodexSkills ".git")) { + Write-Host "removing stale .git from codex dir (junction model needs no second checkout)" + if (-not $DryRun) { Remove-Item -Recurse -Force (Join-Path $CodexSkills ".git") } } -if ($needBootstrap) { - Write-Host "bootstrapping mirror as a checkout of $RepoUrl" - if ($DryRun) { Write-Host "(dry-run) would git init + fetch + align to origin/$Branch"; exit 0 } - Invoke-Git $CodexSkills @("init") | Out-Null - Invoke-Git $CodexSkills @("remote", "add", "origin", $RepoUrl) | Out-Null - Invoke-Git $CodexSkills @("remote", "set-url", "origin", $RepoUrl) | Out-Null - $r = Invoke-Git $CodexSkills @("fetch", "origin", $Branch) - if ($r.Code -ne 0) { - # Flaky server on a full first fetch is common; the primary checkout already - # holds every object locally, so borrow them from disk instead. - Write-Host "server fetch failed, retrying from local primary checkout..." - $r = Invoke-Git $CodexSkills @("fetch", $ClaudeSkills, "+refs/heads/${Branch}:refs/remotes/origin/$Branch") - if ($r.Code -ne 0) { Write-Host $r.Out; Fail "fetch failed during bootstrap (server and local)." } - } - # Point index at origin/main without touching files, so status shows local-vs-repo drift. - Invoke-Git $CodexSkills @("reset", "origin/$Branch") | Out-Null - Invoke-Git $CodexSkills @("branch", "-f", $Branch, "origin/$Branch") | Out-Null - Invoke-Git $CodexSkills @("checkout", $Branch) 2>$null | Out-Null - $drift = Invoke-Git $CodexSkills @("status", "--porcelain") - # Only M is real local drift worth rescuing; D just means the file hasn't been - # materialized in this checkout yet, so overwriting loses nothing. - $modified = ($drift.Out -split "`n" | Where-Object { $_ -match '^\s*M' }) - if ($modified -and -not $Force) { - Write-Host "these tracked files differ locally from the repo:" - $modified | ForEach-Object { Write-Host " $_" } - Fail "codex dir has local drift. Rescue anything worth keeping into the primary checkout, then re-run with -Force." - } - $r = Invoke-Git $CodexSkills @("checkout", "-f", $Branch) - if ($r.Code -ne 0) { Write-Host $r.Out; Fail "checkout failed during bootstrap." } - Invoke-Git $CodexSkills @("branch", "--set-upstream-to=origin/$Branch", $Branch) | Out-Null - Write-Host "mirror bootstrapped at origin/$Branch (untracked codex-local files left untouched)" -} else { - if ($DryRun) { - Invoke-Git $CodexSkills @("fetch", "origin", $Branch) | Out-Null - $r = Invoke-Git $CodexSkills @("log", "--oneline", "HEAD..origin/$Branch") - Write-Host "(dry-run) mirror is behind by:" - if ($r.Out) { Write-Host $r.Out } else { Write-Host " (nothing, or not yet a checkout)" } - } else { - $st2 = Invoke-Git $CodexSkills @("status", "--porcelain") - if ($st2.Out) { - Write-Host "WARNING: mirror has local changes (should be edited on the primary side instead):" - Write-Host $st2.Out +$primarySkills = Get-ChildItem $ClaudeSkills -Directory | Where-Object { $_.Name -notlike ".*" } +$blocked = @() +foreach ($d in $primarySkills) { + $link = Join-Path $CodexSkills $d.Name + if (Test-Path $link) { + $item = Get-Item $link + if ($item.LinkType -eq "Junction") { continue } + # A real dir shadows the primary skill; its content may be unique. + if ($Force) { + if ($DryRun) { Write-Host "(dry-run) would replace real dir with junction: $($d.Name)" ; continue } + Remove-Item -Recurse -Force $link + New-Item -ItemType Junction -Path $link -Target $d.FullName | Out-Null + Write-Host "replaced real dir with junction: $($d.Name)" + } else { + $blocked += $d.Name } - $r = Invoke-Git $CodexSkills @("pull", "--rebase", "origin", $Branch) - if ($r.Code -ne 0) { Write-Host $r.Out; Fail "pull failed on mirror." } - Write-Host $r.Out - Write-Host "mirror in sync with origin/$Branch" + } else { + if ($DryRun) { Write-Host "(dry-run) would link: $($d.Name)"; continue } + New-Item -ItemType Junction -Path $link -Target $d.FullName | Out-Null + Write-Host "linked: $($d.Name)" } } -Write-Host "=== sync_repos done ===" +# Junctions whose primary target is gone (skill deleted/renamed upstream) confuse Codex. +foreach ($item in Get-ChildItem $CodexSkills -Directory | Where-Object { $_.LinkType -eq "Junction" }) { + $target = ($item.Target | Select-Object -First 1) + if ($target -and $target.StartsWith($ClaudeSkills) -and -not (Test-Path $target)) { + if ($DryRun) { Write-Host "(dry-run) would remove stale junction: $($item.Name)"; continue } + # A junction is just a link — removing it never touches target content. + $item.Delete() + Write-Host "removed stale junction: $($item.Name)" + } +} + +if ($blocked) { + Write-Host "these codex dirs are REAL directories shadowing primary skills (not linked):" + $blocked | ForEach-Object { Write-Host " $_" } + Fail "rescue any unique content into the primary checkout, then re-run with -Force." +} + +# Registration sanity: every linked skill must expose a SKILL.md for Codex to register it. +$missing = $primarySkills | Where-Object { -not (Test-Path (Join-Path $CodexSkills "$($_.Name)\SKILL.md")) } +if ($missing) { + Write-Host "WARNING: no SKILL.md visible through these links (Codex will ignore them):" + $missing | ForEach-Object { Write-Host " $($_.Name)" } +} + +Write-Host "=== sync_repos done (Codex picks up changes on its next new session) ==="