Files
skills-win/tools-sync/scripts/sync_repos.ps1
T
2026-07-20 18:14:49 +08:00

114 lines
5.4 KiB
PowerShell

# 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.
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).
[switch]$Force,
# Report what would happen without committing/pushing/checking out.
[switch]$DryRun
)
$ErrorActionPreference = "Stop"
function Invoke-Git {
param([string]$Dir, [string[]]$GitArgs)
# PS 5.1: git writes progress/warnings to stderr; 2>&1 wraps them in ErrorRecords
# which terminate under EAP=Stop. Relax EAP locally and stringify every line.
$prev = $ErrorActionPreference
$ErrorActionPreference = "Continue"
$out = & git -C $Dir @GitArgs 2>&1 | ForEach-Object { "$_" }
$code = $LASTEXITCODE
$ErrorActionPreference = $prev
return @{ Code = $code; Out = (($out -join "`n")).Trim() }
}
function Fail([string]$Msg) { Write-Host "FAIL: $Msg"; exit 1 }
# ---------- Phase 1: primary checkout (~\.claude\skills) ----------
Write-Host "=== [1/2] primary: $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`")."
}
$st = Invoke-Git $ClaudeSkills @("status", "--porcelain")
if ($st.Out) {
Write-Host "local changes:"; Write-Host $st.Out
if ($DryRun) {
Write-Host "(dry-run) would commit these changes"
} else {
Invoke-Git $ClaudeSkills @("add", "-A") | Out-Null
$msg = "sync: $env:COMPUTERNAME $(Get-Date -Format yyyy-MM-dd)"
$r = Invoke-Git $ClaudeSkills @("commit", "-m", $msg)
if ($r.Code -ne 0) { Fail "commit failed:`n$($r.Out)" }
Write-Host "committed: $msg"
}
} else {
Write-Host "working tree clean"
}
if (-not $DryRun) {
$r = Invoke-Git $ClaudeSkills @("pull", "--rebase", "origin", $Branch)
if ($r.Code -ne 0) {
Write-Host $r.Out
Fail "pull --rebase failed on primary. Resolve conflicts (or 'git rebase --abort') then re-run."
}
Write-Host $r.Out
$r = Invoke-Git $ClaudeSkills @("push", "origin", $Branch)
if ($r.Code -ne 0) { Write-Host $r.Out; Fail "push failed (auth? see skill Phase 1)." }
Write-Host "primary in sync with origin/$Branch"
}
# ---------- Phase 2: mirror checkout (~\.codex\skills) ----------
Write-Host "=== [2/2] mirror: $CodexSkills ==="
if (-not (Test-Path $CodexSkills)) { New-Item -ItemType Directory -Force $CodexSkills | Out-Null }
if (-not (Test-Path (Join-Path $CodexSkills ".git"))) {
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
$r = Invoke-Git $CodexSkills @("fetch", "origin", $Branch)
if ($r.Code -ne 0) { Write-Host $r.Out; Fail "fetch failed during bootstrap." }
# 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")
$modified = ($drift.Out -split "`n" | Where-Object { $_ -match '^\s*(M|D)' })
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
}
$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"
}
}
Write-Host "=== sync_repos done ==="