# tools-sync: register Claude Code + Codex skill sources with OpenCode. # # OpenCode auto-discovers ~/.claude/skills and ~/.agents/skills as "global # compatibility" sources (see the OpenCode V2 skills doc), so the primary # checkout and the cross-tool / MATLAB skills need NO extra work -- OpenCode is # already in sync with the master repo and the MATLAB junctions. # # ~/.codex/skills is NOT auto-discovered. This script makes sure the OpenCode # global config (~/.config/opencode/opencode.jsonc) lists it in the `skills` # array, which exposes Codex-only skills (e.g. a project-level dpd-ctl-flow) # to OpenCode. NOTE: that also pulls in Codex's own ~/.codex/skills/.system # built-ins (imagegen / openai-docs / review-agent / skill-creator / # skill-installer) because OpenCode recurses into subdirectories. That is an # accepted, documented side effect; tools-sync still never edits .system. # # It also reports on OpenCode's own global skills dir # (~/.config/opencode/skills): a real skill directory created there is # host-owned and must be INGESTED into the primary checkout by phase 2 of the # main sync. This script only reports it, it never moves anything. # # Exit codes: 0 = already registered (or fixed under -Apply), 1 = hard failure, # 2 = not registered (audit mode only, no -Apply given). # # Run from PowerShell. This script is ASCII-only (a literal BOM char in # gitea_doctor.ps1 once silently broke every sync -- see skills_env.md). [CmdletBinding()] param( [string]$OpencodeConfigDir = "$env:USERPROFILE\.config\opencode", [string]$CodexSkillsDir = "$env:USERPROFILE\.codex\skills", [string]$SkillsEntry = "~/.codex/skills", [switch]$Apply ) $ErrorActionPreference = "Stop" function Normalize-PathEntry([string]$p) { if ($p -eq "~" -or $p.StartsWith("~/")) { $p = $env:USERPROFILE + $p.Substring(1) } $p = $p.Replace("\", "/").TrimEnd("/") return $p.ToLowerInvariant() } function ConvertFrom-JsonC([string]$text) { # Minimal JSONC -> JSON: strip // line comments and /* */ block comments # while respecting double-quoted strings (the opencode config is small and # has no escape-heavy strings). Throws on malformed input. $sb = New-Object System.Text.StringBuilder $inString = $false $inLineComment = $false $inBlockComment = $false $i = 0 $n = $text.Length while ($i -lt $n) { $c = $text[$i] $next = if ($i + 1 -lt $n) { $text[$i + 1] } else { "" } if ($inLineComment) { if ($c -eq "`n" -or $c -eq "`r") { $inLineComment = $false; [void]$sb.Append($c) } } elseif ($inBlockComment) { if ($c -eq "*" -and $next -eq "/") { $inBlockComment = $false; $i++ } } elseif ($inString) { [void]$sb.Append($c) if ($c -eq "\") { if ($i + 1 -lt $n) { [void]$sb.Append($next); $i++ } } elseif ($c -eq '"') { $inString = $false } } else { if ($c -eq '"') { $inString = $true; [void]$sb.Append($c) } elseif ($c -eq "/" -and $next -eq "/") { $inLineComment = $true; $i++ } elseif ($c -eq "/" -and $next -eq "*") { $inBlockComment = $true; $i++ } else { [void]$sb.Append($c) } } $i++ } return ($sb.ToString() | ConvertFrom-Json) } # --- locate config file (jsonc preferred, json fallback) --- $configPath = Join-Path $OpencodeConfigDir "opencode.jsonc" if (-not (Test-Path $configPath -PathType Leaf)) { $jsonPath = Join-Path $OpencodeConfigDir "opencode.json" if (Test-Path $jsonPath -PathType Leaf) { $configPath = $jsonPath } } $target = Normalize-PathEntry $SkillsEntry $codexExists = Test-Path $CodexSkillsDir Write-Host "OpenCode config : $configPath" Write-Host "Skills entry : $SkillsEntry" if (-not $codexExists) { Write-Warning "$CodexSkillsDir does not exist; the entry will be registered but resolves to nothing." } $configExists = Test-Path $configPath -PathType Leaf $root = $null if ($configExists) { $raw = Get-Content $configPath -Raw if ([string]::IsNullOrWhiteSpace($raw)) { $root = [pscustomobject]@{} } else { $root = ConvertFrom-JsonC $raw } } else { $root = [pscustomobject]@{} } # --- inspect current skills array --- $skills = $root.skills if ($null -eq $skills) { $skillList = @() } elseif ($skills -is [string]) { $skillList = @($skills) } elseif ($skills -is [System.Array]) { $skillList = @($skills) } else { $skillList = @($skills | ForEach-Object { [string]$_ }) } $registered = $false foreach ($s in $skillList) { if ((Normalize-PathEntry ([string]$s)) -eq $target) { $registered = $true; break } } # --- report on OpenCode's own global skills dir --- $ownDir = Join-Path $OpencodeConfigDir "skills" $ownReal = @() if (Test-Path $ownDir -PathType Container) { $ownReal = Get-ChildItem $ownDir -Directory -Force | Where-Object { $_.Name -notlike ".*" -and (Test-Path (Join-Path $_.FullName "SKILL.md") -PathType Leaf) } | Select-Object -ExpandProperty Name } if ($registered) { Write-Host "Result : ~/.codex/skills already registered. Nothing to do." } else { Write-Host "Result : ~/.codex/skills NOT registered." } if ($ownReal.Count -gt 0) { Write-Warning ("OpenCode-owned skills in {0} (not junctions, need ingest into primary checkout): {1}" -f $ownDir, ($ownReal -join ", ")) } else { Write-Host "Own skills dir : none (or empty) in $ownDir" } if ($registered) { exit 0 } if (-not $Apply) { Write-Host "Run with -Apply to add the entry to the config." exit 2 } # --- write back under -Apply --- if (-not (Test-Path $OpencodeConfigDir -PathType Container)) { New-Item -ItemType Directory -Path $OpencodeConfigDir -Force | Out-Null } $backup = "$configPath.bak-" + (Get-Date -Format "yyyyMMdd-HHmmss") if ($configExists) { Copy-Item $configPath $backup -Force Write-Host "Backed up : $backup" } $skillList = @($skillList) + @($SkillsEntry) $root | Add-Member -MemberType NoteProperty -Name "skills" -Value $skillList -Force # Re-serialize. Preserve any existing top-level keys, keep the published schema, # and prepend a short ASCII header comment documenting who manages the file. $json = $root | ConvertTo-Json -Depth 10 $header = @( "// OpenCode skill sources. ~/.claude/skills and ~/.agents/skills are auto-", "// discovered (global compatibility sources); ~/.codex/skills is registered", "// here so Codex-only skills are visible. Managed by tools-sync", "// (scripts/sync_opencode_skills.ps1)." ) $out = ($header -join "`n") + "`n" + $json + "`n" Set-Content -Path $configPath -Value $out -Encoding UTF8 Write-Host "Written : $configPath (skills now includes $SkillsEntry)" Write-Host "NOTE: restart OpenCode (opencode reload) or start a new session to pick it up." exit 0