sync: SHANGHAI-U9 2026-10-12 tools-sync 增 OpenCode skills 注册(阶段 2.5 + sync_opencode_skills.ps1 + env 模板)
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user