$ErrorActionPreference = 'Stop' $taskRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) $pluginRoot = Join-Path $taskRoot 'plugins\holodori-web-assistant' $skillRoot = Join-Path $pluginRoot 'skills\holodori-web-assistant' $outputRoot = Join-Path $taskRoot 'plugin-dist' $manifest = Get-Content -LiteralPath (Join-Path $pluginRoot 'plugin.json') -Raw -Encoding UTF8 | ConvertFrom-Json if ($manifest.name -ne 'holodori-web-assistant' -or $manifest.version -notmatch '^\d+\.\d+\.\d+$') { throw 'Invalid plugin identity.' } if ($manifest.extensions.'com.openai'.interface.shortDescription.Length -gt 30) { throw 'Plugin subtitle is too long.' } $allowed = @('plugin.json', 'assets/icon.svg', 'skills/holodori-web-assistant/SKILL.md', 'skills/holodori-web-assistant/agents/openai.yaml', 'skills/holodori-web-assistant/references/requests.md', 'skills/holodori-web-assistant/references/browser-workflow.md') $sourceFiles = @(Get-ChildItem -LiteralPath $pluginRoot -Recurse -File) $paths = @($sourceFiles | ForEach-Object { [System.IO.Path]::GetRelativePath($pluginRoot, $_.FullName).Replace('\', '/') }) if ($paths.Count -ne $allowed.Count -or (Compare-Object $paths $allowed)) { throw 'The instruction package must contain only its declared files.' } $skill = Get-Content -LiteralPath (Join-Path $skillRoot 'SKILL.md') -Raw -Encoding UTF8 if ($skill -notmatch '^---\r?\nname: holodori-web-assistant\r?\ndescription: [^\r\n]+\r?\n---') { throw 'Invalid skill frontmatter.' } New-Item -ItemType Directory -Path $outputRoot -Force | Out-Null $pluginZip = Join-Path $outputRoot 'holodori-web-assistant-plugin.zip' $skillZip = Join-Path $outputRoot 'holodori-web-assistant-skill.zip' Compress-Archive -Path (Join-Path $pluginRoot '*') -DestinationPath $pluginZip -Force Compress-Archive -LiteralPath $skillRoot -DestinationPath $skillZip -Force Add-Type -AssemblyName System.IO.Compression.FileSystem foreach ($archive in @($pluginZip, $skillZip)) { $zip = [System.IO.Compression.ZipFile]::OpenRead($archive) try { $expected = if ($archive -eq $pluginZip) { $allowed } else { @('holodori-web-assistant/SKILL.md', 'holodori-web-assistant/agents/openai.yaml', 'holodori-web-assistant/references/requests.md', 'holodori-web-assistant/references/browser-workflow.md') } $actual = @($zip.Entries | Where-Object { -not $_.FullName.EndsWith('/') } | ForEach-Object { $_.FullName }) if (Compare-Object $actual $expected) { throw 'Unexpected ZIP contents.' } foreach ($entry in $zip.Entries) { if ($entry.FullName.EndsWith('/')) { continue } $relative = if ($archive -eq $pluginZip) { $entry.FullName } else { 'skills/' + $entry.FullName } $source = Join-Path $pluginRoot $relative $stream = $entry.Open() $hasher = [System.Security.Cryptography.SHA256]::Create() try { $archiveHash = [Convert]::ToHexString($hasher.ComputeHash($stream)) } finally { $stream.Dispose(); $hasher.Dispose() } if ($archiveHash -ne (Get-FileHash -LiteralPath $source -Algorithm SHA256).Hash) { throw 'Archive hash mismatch.' } } Write-Output "Verified $archive ($($actual.Count) files; $((Get-Item -LiteralPath $archive).Length) bytes)." } finally { $zip.Dispose() } } $downloadRoot = Join-Path $taskRoot 'static\downloads' New-Item -ItemType Directory -Path $downloadRoot -Force | Out-Null foreach ($archive in @($pluginZip, $skillZip)) { $destination = Join-Path $downloadRoot ([System.IO.Path]::GetFileName($archive)) Copy-Item -LiteralPath $archive -Destination $destination -Force if ((Get-FileHash -LiteralPath $archive).Hash -ne (Get-FileHash -LiteralPath $destination).Hash) { throw 'Download copy hash mismatch.' } Write-Output "Prepared website download: $destination" } $guideRoot = Join-Path $taskRoot 'static\ai' New-Item -ItemType Directory -Path $guideRoot -Force | Out-Null $guide = $skill.Replace('[references/browser-workflow.md](references/browser-workflow.md)', '[Browser calculation workflow](#browser-calculation-workflow)').Replace('[references/requests.md](references/requests.md)', '[Analysis requests](#analysis-requests)') foreach ($reference in @('browser-workflow.md', 'requests.md')) { $guide += "`n`n" + (Get-Content -LiteralPath (Join-Path $skillRoot "references\$reference") -Raw -Encoding UTF8) } if ($guide -match '\]\(references/') { throw 'The standalone guide must not require separate reference files.' } $guidePath = Join-Path $guideRoot 'SKILL.md' [System.IO.File]::WriteAllText($guidePath, $guide, [System.Text.UTF8Encoding]::new($false)) $escapedGuide = [System.Net.WebUtility]::HtmlEncode($guide) $html = @"
GPT용 사용 안내입니다. 아래 내용을 읽고 원래 계산기 탭에서 작업하세요. SKILL.md 다운로드
$escapedGuide"@ [System.IO.File]::WriteAllText((Join-Path $guideRoot 'SKILL.html'), $html, [System.Text.UTF8Encoding]::new($false)) if ((Get-Content -LiteralPath $guidePath -Raw -Encoding UTF8) -ne $guide) { throw 'Standalone guide content mismatch.' } Write-Output "Prepared standalone AI guide: $guidePath"