omni_search 0.2.5

A unified Rust SDK for multimodal embedding and similarity search.
Documentation
[CmdletBinding()]
param(
    [Parameter(Mandatory = $true, Position = 0)]
    [ValidatePattern('^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$')]
    [string]$Version,

    [string]$Remote = "origin",

    [switch]$SkipPublish,

    [switch]$SkipPush
)

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

function Invoke-External {
    param(
        [Parameter(Mandatory = $true)]
        [string]$FilePath,

        [Parameter(Mandatory = $true)]
        [string[]]$ArgumentList
    )

    Write-Host ">> $FilePath $($ArgumentList -join ' ')"
    & $FilePath @ArgumentList
    if ($LASTEXITCODE -ne 0) {
        throw "Command failed: $FilePath $($ArgumentList -join ' ')"
    }
}

function Get-ExternalOutput {
    param(
        [Parameter(Mandatory = $true)]
        [string]$FilePath,

        [Parameter(Mandatory = $true)]
        [string[]]$ArgumentList
    )

    $output = & $FilePath @ArgumentList 2>&1
    if ($LASTEXITCODE -ne 0) {
        $message = ($output | Out-String).Trim()
        throw "Command failed: $FilePath $($ArgumentList -join ' ')`n$message"
    }

    return ($output | Out-String).Trim()
}

function Get-ManifestPath {
    $repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
    return (Join-Path $repoRoot "Cargo.toml")
}

function Get-ChangelogPath {
    $repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
    return (Join-Path $repoRoot "docs/CHANGELOG.md")
}

function Get-PackageVersion {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ManifestPath
    )

    $manifest = Get-Content $ManifestPath -Raw
    $match = [regex]::Match(
        $manifest,
        '(?ms)^\[package\]\s*$.*?^version\s*=\s*"([^"]+)"\s*$'
    )

    if (-not $match.Success) {
        throw "Could not find [package].version in Cargo.toml."
    }

    return $match.Groups[1].Value
}

function Set-PackageVersion {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ManifestPath,

        [Parameter(Mandatory = $true)]
        [string]$NewVersion
    )

    $manifest = Get-Content $ManifestPath -Raw
    $updated = [regex]::Replace(
        $manifest,
        '(?ms)(^\[package\]\s*$.*?^version\s*=\s*")([^"]+)(".*$)',
        ('${1}' + $NewVersion + '${3}'),
        1
    )

    if ($updated -eq $manifest) {
        throw "Failed to update [package].version in Cargo.toml."
    }

    $encoding = [System.Text.UTF8Encoding]::new($false)
    [System.IO.File]::WriteAllText($ManifestPath, $updated, $encoding)
}

function Update-ChangelogForRelease {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ChangelogPath,

        [Parameter(Mandatory = $true)]
        [string]$Version
    )

    if (-not (Test-Path -LiteralPath $ChangelogPath)) {
        throw "CHANGELOG.md not found at $ChangelogPath."
    }

    $content = Get-Content $ChangelogPath -Raw
    $newline = if ($content.Contains("`r`n")) { "`r`n" } else { "`n" }
    $normalized = $content -replace "`r`n", "`n"

    $unreleasedMatch = [regex]::Match($normalized, '(?m)^## \[Unreleased\]\s*$')
    if (-not $unreleasedMatch.Success) {
        throw "docs/CHANGELOG.md is missing the ## [Unreleased] section."
    }

    $unreleasedBodyStart = $unreleasedMatch.Index + $unreleasedMatch.Length
    $afterUnreleased = $normalized.Substring($unreleasedBodyStart)
    $nextSectionMatch = [regex]::Match($afterUnreleased, '(?m)^## \[')

    if ($nextSectionMatch.Success) {
        $unreleasedBody = $afterUnreleased.Substring(0, $nextSectionMatch.Index)
        $remainingSections = $afterUnreleased.Substring($nextSectionMatch.Index)
    }
    else {
        $unreleasedBody = $afterUnreleased
        $remainingSections = ""
    }

    $categories = @("Added", "Changed", "Deprecated", "Removed", "Fixed", "Security")
    $categoryPattern = '(?ms)^### (?<name>Added|Changed|Deprecated|Removed|Fixed|Security)[ \t]*$\n?(?<body>.*?)(?=^### (?:Added|Changed|Deprecated|Removed|Fixed|Security)[ \t]*$|\z)'
    $categoryBodies = @{}
    foreach ($category in $categories) {
        $categoryBodies[$category] = ""
    }

    $trimmedUnreleasedBody = $unreleasedBody.Trim("`n")
    foreach ($match in [regex]::Matches($trimmedUnreleasedBody, $categoryPattern)) {
        $categoryBodies[$match.Groups["name"].Value] = $match.Groups["body"].Value.Trim()
    }

    $unsupportedContent = [regex]::Replace($trimmedUnreleasedBody, $categoryPattern, "").Trim()
    if (-not [string]::IsNullOrWhiteSpace($unsupportedContent)) {
        throw "docs/CHANGELOG.md contains unsupported content under Unreleased. Only standard category sections are supported."
    }

    $releaseBlocks = New-Object System.Collections.Generic.List[string]
    foreach ($category in $categories) {
        $body = $categoryBodies[$category]
        if (-not [string]::IsNullOrWhiteSpace($body)) {
            $releaseBlocks.Add("### $category`n`n$body")
        }
    }

    if ($releaseBlocks.Count -eq 0) {
        throw "docs/CHANGELOG.md does not contain any Unreleased entries to release."
    }

    $releaseDate = Get-Date -Format "yyyy-MM-dd"
    $emptyUnreleased = @(
        "## [Unreleased]",
        "",
        "### Added",
        "",
        "### Changed",
        "",
        "### Deprecated",
        "",
        "### Removed",
        "",
        "### Fixed",
        "",
        "### Security"
    ) -join "`n"

    $releaseSection = @(
        "## [$Version] - $releaseDate",
        "",
        ($releaseBlocks -join "`n`n")
    ) -join "`n"

    $prefix = $normalized.Substring(0, $unreleasedMatch.Index)
    $updated = @(
        $prefix.TrimEnd("`n"),
        "",
        $emptyUnreleased,
        "",
        $releaseSection
    ) -join "`n"

    if (-not [string]::IsNullOrWhiteSpace($remainingSections)) {
        $updated = @(
            $updated.TrimEnd("`n"),
            "",
            $remainingSections.TrimStart("`n")
        ) -join "`n"
    }

    if ($content.EndsWith("`r`n")) {
        $updated = $updated.TrimEnd("`n") + "`n"
    }
    elseif ($content.EndsWith("`n")) {
        $updated = $updated.TrimEnd("`n") + "`n"
    }

    $encoding = [System.Text.UTF8Encoding]::new($false)
    [System.IO.File]::WriteAllText($ChangelogPath, ($updated -replace "`n", $newline), $encoding)
}

function Assert-CleanWorktree {
    $status = Get-ExternalOutput git @("status", "--short")
    if ($status) {
        throw "Git worktree is not clean. Commit or stash changes before running the release script."
    }
}

function Assert-RemoteExists {
    param(
        [Parameter(Mandatory = $true)]
        [string]$RemoteName
    )

    $null = Get-ExternalOutput git @("remote", "get-url", $RemoteName)
}

function Assert-TagDoesNotExist {
    param(
        [Parameter(Mandatory = $true)]
        [string]$RemoteName,

        [Parameter(Mandatory = $true)]
        [string]$TagName
    )

    & git rev-parse --verify --quiet "refs/tags/$TagName" *> $null
    if ($LASTEXITCODE -eq 0) {
        throw "Tag $TagName already exists locally."
    }

    $remoteTag = Get-ExternalOutput git @("ls-remote", "--tags", $RemoteName, "refs/tags/$TagName")
    if ($remoteTag) {
        throw "Tag $TagName already exists on remote $RemoteName."
    }
}

function Get-CurrentBranch {
    $branch = Get-ExternalOutput git @("branch", "--show-current")
    if (-not $branch) {
        throw "Detached HEAD is not supported for releases."
    }

    return $branch
}

if ($SkipPublish -and -not $SkipPush) {
    throw "-SkipPublish requires -SkipPush. Pushing a release tag without publishing the crate is not supported."
}

$manifestPath = Get-ManifestPath
$changelogPath = Get-ChangelogPath
$repoRoot = Split-Path $manifestPath -Parent
Set-Location $repoRoot

Assert-CleanWorktree
Assert-RemoteExists -RemoteName $Remote

$currentVersion = Get-PackageVersion -ManifestPath $manifestPath
if ($currentVersion -eq $Version) {
    throw "Cargo.toml is already at version $Version."
}

$tagName = "v$Version"
Assert-TagDoesNotExist -RemoteName $Remote -TagName $tagName

$branch = Get-CurrentBranch

Write-Host "Updating Cargo.toml version: $currentVersion -> $Version"
Set-PackageVersion -ManifestPath $manifestPath -NewVersion $Version
Write-Host "Updating docs/CHANGELOG.md for release $Version"
Update-ChangelogForRelease -ChangelogPath $changelogPath -Version $Version

Invoke-External cargo @("test")
Invoke-External cargo @("publish", "--dry-run", "--locked", "--allow-dirty")

Invoke-External git @("add", "Cargo.toml", "Cargo.lock", "docs/CHANGELOG.md")
Invoke-External git @("commit", "-m", $Version)

if (-not $SkipPublish) {
    Invoke-External cargo @("publish", "--locked")
}

Invoke-External git @("tag", $tagName)

if (-not $SkipPush) {
    Invoke-External git @("push", $Remote, $branch)
    Invoke-External git @("push", $Remote, $tagName)
}

Write-Host "Release $Version completed."