Localize marketplace metadata and group Three.js skills

Constraint: Chinese users need localized plugin and skill descriptions while third-party sources must remain syncable from upstream.

Rejected: Editing third-party generated files by hand | Sync would overwrite them and make localization drift from config.

Rejected: One plugin per Three.js skill | Users should install one domain package while Codex can still select individual skills inside it.

Confidence: high

Scope-risk: moderate

Directive: Keep plugin IDs and skill names in English kebab-case; put user-facing Chinese text in manifest fields, SKILL.md descriptions, or external-source overrides.

Tested: Codex manual confirms duplicate skills are not merged; powershell sync_external_plugins.ps1 with proxy; python scripts\\validate_marketplace.py; validate_plugin.py for all plugins; codex plugin list local marketplace; codex plugin add/remove eapil-threejs.

Not-tested: Codex Desktop visual refresh after pulling the updated remote marketplace.
This commit is contained in:
AreChen
2026-06-10 16:12:27 +08:00
parent 64f469cfce
commit bfe3275e21
50 changed files with 469 additions and 744 deletions
+123 -5
View File
@@ -69,7 +69,8 @@ function Copy-PathIfExists {
function Read-JsonFile {
param([string]$Path)
return Get-Content -Raw -LiteralPath $Path | ConvertFrom-Json
$resolvedPath = (Resolve-Path -LiteralPath $Path).Path
return [System.IO.File]::ReadAllText($resolvedPath, [System.Text.Encoding]::UTF8) | ConvertFrom-Json
}
function Write-JsonFile {
@@ -79,17 +80,30 @@ function Write-JsonFile {
[System.IO.File]::WriteAllText($Path, "$json`n", $utf8NoBom)
}
function Write-Utf8NoBomText {
param([string]$Path, [string]$Text)
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($Path, $Text, $utf8NoBom)
}
function ConvertTo-Hashtable {
param([object]$InputObject)
if ($null -eq $InputObject) {
return $null
}
if ($InputObject -is [System.Collections.IDictionary]) {
$hash = [ordered]@{}
foreach ($key in $InputObject.Keys) {
$hash[$key] = ConvertTo-Hashtable $InputObject[$key]
}
return $hash
}
if ($InputObject -is [System.Collections.IEnumerable] -and -not ($InputObject -is [string]) -and -not ($InputObject -is [System.Management.Automation.PSCustomObject])) {
$items = @()
foreach ($item in $InputObject) {
$items += ConvertTo-Hashtable $item
}
return $items
return ,$items
}
if ($InputObject -is [System.Management.Automation.PSCustomObject]) {
$hash = [ordered]@{}
@@ -103,7 +117,7 @@ function ConvertTo-Hashtable {
function Get-SkillFrontmatter {
param([string]$SkillFile)
$lines = Get-Content -LiteralPath $SkillFile
$lines = [System.IO.File]::ReadAllLines((Resolve-Path -LiteralPath $SkillFile).Path, [System.Text.Encoding]::UTF8)
if ($lines.Count -lt 3 -or $lines[0] -ne "---") {
return @{}
}
@@ -121,6 +135,108 @@ function Get-SkillFrontmatter {
return $frontmatter
}
function Merge-Hashtable {
param(
[object]$Base,
[object]$Override
)
$baseHash = ConvertTo-Hashtable $Base
$overrideHash = ConvertTo-Hashtable $Override
foreach ($key in $overrideHash.Keys) {
if ($baseHash.Contains($key) -and $baseHash[$key] -is [System.Collections.IDictionary] -and $overrideHash[$key] -is [System.Collections.IDictionary]) {
$baseHash[$key] = Merge-Hashtable -Base $baseHash[$key] -Override $overrideHash[$key]
} else {
$baseHash[$key] = $overrideHash[$key]
}
}
return $baseHash
}
function Get-SourceCategory {
param([object]$Source)
if ($Source.PSObject.Properties.Name -contains "marketplace" -and $Source.marketplace -and $Source.marketplace.category) {
return $Source.marketplace.category
}
return $Source.category
}
function ConvertTo-YamlDoubleQuoted {
param([string]$Value)
$escaped = $Value.Replace("\", "\\").Replace('"', '\"')
return "`"$escaped`""
}
function Set-SkillFrontmatterValue {
param(
[string]$SkillFile,
[string]$Key,
[string]$Value
)
$lines = @([System.IO.File]::ReadAllLines((Resolve-Path -LiteralPath $SkillFile).Path, [System.Text.Encoding]::UTF8))
if ($lines.Count -lt 3 -or $lines[0] -ne "---") {
throw "Cannot update frontmatter in $SkillFile"
}
$endIndex = -1
for ($i = 1; $i -lt $lines.Count; $i++) {
if ($lines[$i] -eq "---") {
$endIndex = $i
break
}
}
if ($endIndex -lt 0) {
throw "Cannot find frontmatter end in $SkillFile"
}
$replacement = "$Key`: $(ConvertTo-YamlDoubleQuoted $Value)"
$updated = $false
for ($i = 1; $i -lt $endIndex; $i++) {
if ($lines[$i] -match "^\s*$([regex]::Escape($Key))\s*:") {
$lines[$i] = $replacement
$updated = $true
break
}
}
if (-not $updated) {
$before = @()
if ($endIndex -gt 1) {
$before = $lines[0..($endIndex - 1)]
} else {
$before = @($lines[0])
}
$after = $lines[$endIndex..($lines.Count - 1)]
$lines = @($before + $replacement + $after)
}
Write-Utf8NoBomText -Path $SkillFile -Text (($lines -join "`n") + "`n")
}
function Apply-LocalOverrides {
param(
[string]$PluginDir,
[object]$Source
)
if ($Source.PSObject.Properties.Name -contains "manifestOverrides" -and $Source.manifestOverrides) {
$manifestPath = Join-Path $PluginDir ".codex-plugin\plugin.json"
$manifest = ConvertTo-Hashtable (Read-JsonFile $manifestPath)
$manifest = Merge-Hashtable -Base $manifest -Override $Source.manifestOverrides
Write-JsonFile -Path $manifestPath -Value $manifest
}
if ($Source.PSObject.Properties.Name -contains "skillDescriptions" -and $Source.skillDescriptions) {
$skillsRoot = Join-Path $PluginDir "skills"
foreach ($property in $Source.skillDescriptions.PSObject.Properties) {
$skillFile = Join-Path (Join-Path $skillsRoot $property.Name) "SKILL.md"
if (-not (Test-Path -LiteralPath $skillFile)) {
throw "skillDescriptions references missing skill: $skillFile"
}
Set-SkillFrontmatterValue -SkillFile $skillFile -Key "description" -Value $property.Value
}
}
}
function Get-Clone {
param(
[object]$Source,
@@ -201,6 +317,7 @@ function Sync-CodexPlugin {
$dst = Join-Path $pluginDir $include
Copy-PathIfExists -Source $src -Destination $dst -Within $RepoRoot
}
Apply-LocalOverrides -PluginDir $pluginDir -Source $Source
Write-Provenance -PluginDir $pluginDir -Source $Source -Commit $Commit -SyncedAt $SyncedAt -SourcePath $relativeSourcePath
}
@@ -301,7 +418,7 @@ function Sync-ClaudeSkill {
shortDescription = $description
longDescription = $description
developerName = (New-AuthorObject $metadata.author).name
category = $Source.category
category = (Get-SourceCategory $Source)
capabilities = @("Read", "Write")
defaultPrompt = @("Use $displayName for this task.")
websiteURL = $homepage
@@ -312,6 +429,7 @@ function Sync-ClaudeSkill {
}
}
Write-JsonFile -Path (Join-Path $pluginDir ".codex-plugin\plugin.json") -Value $manifest
Apply-LocalOverrides -PluginDir $pluginDir -Source $Source
Write-Provenance -PluginDir $pluginDir -Source $Source -Commit $Commit -SyncedAt $SyncedAt -SourcePath $Source.skillPath
}
@@ -340,7 +458,7 @@ function Update-Marketplace {
installation = "AVAILABLE"
authentication = "ON_INSTALL"
}
category = $source.category
category = (Get-SourceCategory $source)
}
}
$marketplace.plugins = $plugins