forked from Crockan/MercuryToolbox
chore(release): prepare public source release
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateSet('Debug', 'Release', 'ReleaseFast', 'ReleaseSize')]
|
||||
[string]$Configuration = 'ReleaseFast',
|
||||
[string]$OutputPath = (Join-Path (Join-Path (Split-Path -Parent $PSScriptRoot) 'docs') 'ai\mercury-toolbox-ai-prompt.md'),
|
||||
[string]$NotesPath = (Join-Path (Join-Path (Split-Path -Parent $PSScriptRoot) 'docs') 'ai\toolbox-ai-prompt-notes.json'),
|
||||
[switch]$Check,
|
||||
[switch]$SkipBuild
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
. (Join-Path $PSScriptRoot 'toolbox-commands.ps1')
|
||||
|
||||
function Invoke-StrictNative {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$FilePath,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string[]]$ArgumentList
|
||||
)
|
||||
|
||||
& $FilePath @ArgumentList
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Command failed with exit code ${LASTEXITCODE}: $FilePath $($ArgumentList -join ' ')"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ToolHelpSections {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$HelpText
|
||||
)
|
||||
|
||||
$lines = $HelpText -split "`r?`n"
|
||||
$summary = ($lines | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -First 1).Trim()
|
||||
$usage = [System.Collections.Generic.List[string]]::new()
|
||||
$examples = [System.Collections.Generic.List[string]]::new()
|
||||
$section = ''
|
||||
|
||||
foreach ($line in $lines) {
|
||||
$trimmed = $line.Trim()
|
||||
if ($trimmed -ceq 'Usage:') {
|
||||
$section = 'usage'
|
||||
continue
|
||||
}
|
||||
|
||||
if ($trimmed -ceq 'Examples:') {
|
||||
$section = 'examples'
|
||||
continue
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($trimmed)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (Test-HelpSectionHeader -Line $trimmed) {
|
||||
$section = ''
|
||||
continue
|
||||
}
|
||||
|
||||
switch ($section) {
|
||||
'usage' { $usage.Add($trimmed) }
|
||||
'examples' { $examples.Add($trimmed) }
|
||||
}
|
||||
}
|
||||
|
||||
return @{
|
||||
Summary = $summary
|
||||
Usage = @($usage)
|
||||
Examples = @($examples)
|
||||
}
|
||||
}
|
||||
|
||||
function Test-HelpSectionHeader {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Line
|
||||
)
|
||||
|
||||
return $Line -cmatch '^[A-Z][A-Za-z0-9 /_-]*:$'
|
||||
}
|
||||
|
||||
function Append-Section {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Text.StringBuilder]$Builder,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Heading,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string[]]$Items
|
||||
)
|
||||
|
||||
[void]$Builder.AppendLine("## $Heading")
|
||||
[void]$Builder.AppendLine()
|
||||
foreach ($item in $Items) {
|
||||
[void]$Builder.AppendLine("- $item")
|
||||
}
|
||||
[void]$Builder.AppendLine()
|
||||
}
|
||||
|
||||
function Select-PromptExamples {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string[]]$Examples,
|
||||
[int]$Limit = 1
|
||||
)
|
||||
|
||||
$selected = [System.Collections.Generic.List[string]]::new()
|
||||
if ($Examples.Count -eq 0) {
|
||||
return @()
|
||||
}
|
||||
|
||||
foreach ($example in $Examples) {
|
||||
if (-not $example.Contains('Get-Content') -and -not $example.Contains('fixtures')) {
|
||||
$selected.Add($example)
|
||||
break
|
||||
}
|
||||
}
|
||||
if ($selected.Count -eq 0) {
|
||||
foreach ($example in $Examples) {
|
||||
if (-not $example.Contains('Get-Content')) {
|
||||
$selected.Add($example)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($selected.Count -eq 0) {
|
||||
$selected.Add($Examples[0])
|
||||
}
|
||||
|
||||
foreach ($example in $Examples | Select-Object -Skip 1) {
|
||||
if (
|
||||
$selected.Count -lt $Limit -and
|
||||
$example -cne $selected[0] -and
|
||||
-not $example.Contains('Get-Content') -and
|
||||
-not $example.Contains('fixtures') -and
|
||||
($example.Contains('|') -or $example.Contains('ConvertFrom-Json'))
|
||||
) {
|
||||
$selected.Add($example)
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($example in $Examples | Select-Object -Skip 1) {
|
||||
if ($selected.Count -ge $Limit) {
|
||||
break
|
||||
}
|
||||
|
||||
if (
|
||||
-not $selected.Contains($example) -and
|
||||
-not $example.Contains('Get-Content') -and
|
||||
-not $example.Contains('fixtures')
|
||||
) {
|
||||
$selected.Add($example)
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($example in $Examples | Select-Object -Skip 1) {
|
||||
if ($selected.Count -ge $Limit) {
|
||||
break
|
||||
}
|
||||
|
||||
if (-not $selected.Contains($example)) {
|
||||
$selected.Add($example)
|
||||
}
|
||||
}
|
||||
|
||||
return @($selected)
|
||||
}
|
||||
|
||||
function Normalize-GeneratedExample {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Example
|
||||
)
|
||||
|
||||
return ($Example -replace '(?i)(?:[A-Z]:)?[^''"\s|]*fixtures(?:[\\/][^''"\s|]+)+', '<PATH>')
|
||||
}
|
||||
|
||||
function Test-ByteSequenceEqual {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[byte[]]$Left,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[byte[]]$Right
|
||||
)
|
||||
|
||||
if ($Left.Length -ne $Right.Length) {
|
||||
return $false
|
||||
}
|
||||
|
||||
for ($index = 0; $index -lt $Left.Length; $index += 1) {
|
||||
if ($Left[$index] -ne $Right[$index]) {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
return $true
|
||||
}
|
||||
|
||||
function ConvertTo-LfText {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Content
|
||||
)
|
||||
|
||||
return $Content.Replace("`r`n", "`n").Replace("`r", "`n")
|
||||
}
|
||||
|
||||
function Write-Utf8FileWithRetry {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Content,
|
||||
[int]$Attempts = 20,
|
||||
[int]$DelayMilliseconds = 100
|
||||
)
|
||||
|
||||
for ($attempt = 1; $attempt -le $Attempts; $attempt++) {
|
||||
try {
|
||||
$encoding = [System.Text.UTF8Encoding]::new($false)
|
||||
[System.IO.File]::WriteAllText($Path, (ConvertTo-LfText -Content $Content), $encoding)
|
||||
return
|
||||
}
|
||||
catch {
|
||||
if ($attempt -eq $Attempts) {
|
||||
throw
|
||||
}
|
||||
Start-Sleep -Milliseconds $DelayMilliseconds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Select-PromptUsage {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[hashtable]$ToolNotes,
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Help,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$CommandName
|
||||
)
|
||||
|
||||
if ($ToolNotes.ContainsKey('prompt_usage') -and -not [string]::IsNullOrWhiteSpace([string]$ToolNotes.prompt_usage)) {
|
||||
return [string]$ToolNotes.prompt_usage
|
||||
}
|
||||
if ($Help.Usage.Count -gt 0) {
|
||||
return (([string[]]$Help.Usage) -join ' | ')
|
||||
}
|
||||
return "$CommandName --help"
|
||||
}
|
||||
|
||||
$workspaceRoot = Split-Path -Parent $PSScriptRoot
|
||||
$profileName = Resolve-ToolboxProfileName -Configuration $Configuration
|
||||
$binaryRoot = Join-Path $workspaceRoot (Join-Path 'target' $profileName)
|
||||
$dependencyRoot = Join-Path $binaryRoot 'deps'
|
||||
if (Test-Path -LiteralPath $dependencyRoot -PathType Container) {
|
||||
$env:PATH = "$dependencyRoot$([System.IO.Path]::PathSeparator)$env:PATH"
|
||||
}
|
||||
$cargoPath = 'cargo'
|
||||
$notes = Get-Content -Raw -LiteralPath $NotesPath | ConvertFrom-Json -AsHashtable
|
||||
|
||||
$missingBinary = $false
|
||||
foreach ($commandName in Get-ToolboxCommandNames) {
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $binaryRoot "$commandName.exe"))) {
|
||||
$missingBinary = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($missingBinary) {
|
||||
if ($SkipBuild) {
|
||||
throw "Required toolbox binaries are missing under $binaryRoot. Build profile $Configuration before running generate-ai-prompt.ps1 -SkipBuild."
|
||||
}
|
||||
Invoke-ToolboxBuild -CargoPath $cargoPath -Configuration $Configuration
|
||||
}
|
||||
|
||||
$builder = [System.Text.StringBuilder]::new()
|
||||
[void]$builder.AppendLine("# $($notes.title)")
|
||||
[void]$builder.AppendLine(('Available tools: {0}' -f ((Get-ToolboxCommandNames) -join ', ')))
|
||||
[void]$builder.AppendLine(('Rules: {0}' -f (([string[]]$notes.selection_rules) -join ' ')))
|
||||
|
||||
foreach ($commandName in Get-ToolboxCommandNames) {
|
||||
$binaryPath = Join-Path $binaryRoot "$commandName.exe"
|
||||
$toolNotes = $notes.tools[$commandName]
|
||||
if ($null -eq $toolNotes) {
|
||||
throw "Missing AI prompt notes for tool '$commandName'"
|
||||
}
|
||||
|
||||
$helpText = (& $binaryPath '--help' | Out-String).TrimEnd()
|
||||
$help = Get-ToolHelpSections -HelpText $helpText
|
||||
$usageLine = Select-PromptUsage -ToolNotes $toolNotes -Help $help -CommandName $commandName
|
||||
$examples = @(Select-PromptExamples -Examples ([string[]]$help.Examples))
|
||||
$exampleLine = if ($toolNotes.ContainsKey('prompt_example') -and -not [string]::IsNullOrWhiteSpace([string]$toolNotes.prompt_example)) {
|
||||
[string]$toolNotes.prompt_example
|
||||
} elseif ($examples.Count -gt 0) {
|
||||
$examples[0]
|
||||
} else {
|
||||
"$commandName --help"
|
||||
}
|
||||
$exampleLine = Normalize-GeneratedExample -Example $exampleLine
|
||||
$guided = $toolNotes.guided_triage
|
||||
if ($null -eq $guided) {
|
||||
throw "Missing guided_triage notes for tool '$commandName'"
|
||||
}
|
||||
$nextActions = ([string[]]$guided.next_actions) -join ' | '
|
||||
|
||||
[void]$builder.AppendLine(('{0}: {1} Better than: {2} Usage: `{3}` Example: `{4}` Guided: answer={5} trust={6} next={7}' -f $commandName, $toolNotes.use_when, $toolNotes.why, $usageLine, $exampleLine, $guided.answer, $guided.trust, $nextActions))
|
||||
}
|
||||
|
||||
$content = $builder.ToString()
|
||||
|
||||
if ($Check) {
|
||||
if (-not (Test-Path -LiteralPath $OutputPath)) {
|
||||
throw "Generated AI prompt is missing: $OutputPath"
|
||||
}
|
||||
|
||||
$temporaryOutputPath = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName())
|
||||
try {
|
||||
$encoding = [System.Text.UTF8Encoding]::new($false)
|
||||
[System.IO.File]::WriteAllText($temporaryOutputPath, (ConvertTo-LfText -Content $content), $encoding)
|
||||
$existingBytes = [System.IO.File]::ReadAllBytes((Resolve-Path -LiteralPath $OutputPath))
|
||||
$generatedBytes = [System.IO.File]::ReadAllBytes($temporaryOutputPath)
|
||||
if (-not (Test-ByteSequenceEqual -Left $existingBytes -Right $generatedBytes)) {
|
||||
throw "Generated AI prompt is out of date. Run pwsh -NoProfile -File .\scripts\generate-ai-prompt.ps1"
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Remove-Item -LiteralPath $temporaryOutputPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Write-Host "AI prompt is up to date: $OutputPath"
|
||||
return
|
||||
}
|
||||
|
||||
$outputDirectory = Split-Path -Parent $OutputPath
|
||||
New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null
|
||||
Write-Utf8FileWithRetry -Path $OutputPath -Content $content
|
||||
Write-Host "Generated AI prompt: $OutputPath"
|
||||
Reference in New Issue
Block a user