sync: WIN10-I9-64G 2026-07-20
This commit is contained in:
@@ -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) ==="
|
||||
|
||||
Reference in New Issue
Block a user