hematite-cli 0.7.1

Senior SysAdmin, Network Admin, and Software Engineer living in your terminal. A high-precision local AI agent harness for LM Studio, Ollama, and other local OpenAI-compatible runtimes that runs 100% on your own silicon. Reads repos, edits files, runs builds, and inspects the machine it is running on—including full network state and workstation telemetry.
Documentation
[CmdletBinding(DefaultParameterSetName = "ByBump")]
param(
    [Parameter(Mandatory, ParameterSetName = "ByVersion")]
    [string]$Version,

    [Parameter(Mandatory, ParameterSetName = "ByBump")]
    [ValidateSet("patch", "minor", "major")]
    [string]$Bump,

    [switch]$Push,
    [switch]$AddToPath,
    [switch]$SkipInstaller,
    [switch]$PublishCrates,
    [switch]$PublishVoiceCrate
)

$ErrorActionPreference = "Stop"

$repoRoot = $PSScriptRoot
Set-Location $repoRoot

function Invoke-Step([string]$Label, [scriptblock]$Action) {
    Write-Host ""
    Write-Host "==> $Label" -ForegroundColor Cyan
    & $Action
}

function Get-CurrentVersion {
    $cargoToml = Get-Content -LiteralPath "Cargo.toml" -Raw
    $match = [regex]::Match($cargoToml, '(?m)^version\s*=\s*"([^"]+)"')
    if (-not $match.Success) {
        throw "Could not determine package version from Cargo.toml."
    }
    $match.Groups[1].Value
}

function Get-BumpedVersion([string]$CurrentVersion, [string]$BumpKind) {
    $parts = $CurrentVersion.Split('.')
    if ($parts.Length -ne 3) {
        throw "Unsupported semantic version format: $CurrentVersion"
    }

    $major = [int]$parts[0]
    $minor = [int]$parts[1]
    $patch = [int]$parts[2]

    switch ($BumpKind) {
        "patch" { $patch += 1 }
        "minor" { $minor += 1; $patch = 0 }
        "major" { $major += 1; $minor = 0; $patch = 0 }
        default { throw "Unknown bump kind: $BumpKind" }
    }

    "$major.$minor.$patch"
}

function Ensure-CleanWorktree {
    $status = git status --porcelain
    if ($LASTEXITCODE -ne 0) {
        throw "git status failed."
    }
    if ($status) {
        throw "Release flow requires a clean git worktree. Commit or stash existing changes first."
    }
}

function Ensure-TagDoesNotExist([string]$TagName) {
    $existing = git tag --list $TagName
    if ($LASTEXITCODE -ne 0) {
        throw "git tag --list failed."
    }
    if ($existing) {
        throw "Tag $TagName already exists."
    }
}

function Invoke-CargoBuild {
    $previousOffline = $env:CARGO_NET_OFFLINE
    if (Test-Path Env:CARGO_NET_OFFLINE) {
        Remove-Item Env:CARGO_NET_OFFLINE
    }

    try {
        & cargo build
        if ($LASTEXITCODE -ne 0) {
            throw "cargo build failed."
        }
    } finally {
        if ($null -ne $previousOffline) {
            $env:CARGO_NET_OFFLINE = $previousOffline
        }
    }
}

function Invoke-WindowsPackage([bool]$IncludeInstaller, [bool]$RegisterPath) {
    $args = @("-ExecutionPolicy", "Bypass", "-File", ".\scripts\package-windows.ps1")
    if ($IncludeInstaller) {
        $args += "-Installer"
    }
    if ($RegisterPath) {
        $args += "-AddToPath"
    }

    & powershell @args
    if ($LASTEXITCODE -ne 0) {
        throw "Windows packaging failed."
    }
}

function Invoke-UnixPackage {
    & bash ./scripts/package-unix.sh
    if ($LASTEXITCODE -ne 0) {
        throw "Unix packaging failed."
    }
}

function Invoke-CargoPublish([string]$WorkingDirectory) {
    Push-Location $WorkingDirectory
    try {
        & cargo publish
        if ($LASTEXITCODE -ne 0) {
            throw "cargo publish failed in $WorkingDirectory."
        }
    } finally {
        Pop-Location
    }
}

$currentVersion = Get-CurrentVersion
if ($PSCmdlet.ParameterSetName -eq "ByBump") {
    $Version = Get-BumpedVersion -CurrentVersion $currentVersion -BumpKind $Bump
}

if ($PublishVoiceCrate -and -not $PublishCrates) {
    throw "-PublishVoiceCrate requires -PublishCrates."
}

if ($PublishCrates -and -not $Push) {
    throw "-PublishCrates requires -Push so the published crates match a pushed commit and tag."
}

$tagName = "v$Version"

Write-Host "Preparing release $Version from $currentVersion" -ForegroundColor Yellow
if ($Version -eq $currentVersion) {
    throw "Target version matches the current version. Pick a new version or bump kind."
}

Invoke-Step "Checking worktree state" {
    Ensure-CleanWorktree
    Ensure-TagDoesNotExist $tagName
}

Invoke-Step "Bumping version metadata" {
    & powershell -ExecutionPolicy Bypass -File .\bump-version.ps1 -Version $Version
    if ($LASTEXITCODE -ne 0) {
        throw "Version bump failed."
    }
}

Invoke-Step "Rebuilding debug artifacts and Cargo.lock" {
    Invoke-CargoBuild
}

Invoke-Step "Verifying version sync" {
    & powershell -ExecutionPolicy Bypass -File .\scripts\verify-version-sync.ps1 -Version $Version -RequireCargoLock
    if ($LASTEXITCODE -ne 0) {
        throw "Version sync verification failed."
    }
}

Invoke-Step "Creating version bump commit" {
    git add Cargo.toml Cargo.lock README.md CLAUDE.md installer/hematite.iss
    if ($LASTEXITCODE -ne 0) {
        throw "git add failed."
    }

    git commit -m "chore: bump version to $Version"
    if ($LASTEXITCODE -ne 0) {
        throw "git commit failed."
    }
}

Invoke-Step "Creating release tag" {
    git tag -a $tagName -m "Release $tagName"
    if ($LASTEXITCODE -ne 0) {
        throw "git tag failed."
    }
}

Invoke-Step "Building release artifacts" {
    if ($IsWindows -or $env:OS -eq "Windows_NT") {
        Invoke-WindowsPackage -IncludeInstaller:(-not $SkipInstaller) -RegisterPath:$AddToPath
    } else {
        Invoke-UnixPackage
    }
}

if ($Push) {
    Invoke-Step "Pushing commit and tag" {
        git push origin main
        if ($LASTEXITCODE -ne 0) {
            throw "git push origin main failed."
        }

        git push origin $tagName
        if ($LASTEXITCODE -ne 0) {
            throw "git push origin $tagName failed."
        }
    }
}

if ($PublishCrates) {
    if ($PublishVoiceCrate) {
        Invoke-Step "Publishing hematite-kokoros to crates.io" {
            Invoke-CargoPublish (Join-Path $repoRoot "libs\kokoros")
        }
    }

    Invoke-Step "Publishing hematite-cli to crates.io" {
        Invoke-CargoPublish $repoRoot
    }
}

if (-not $Push) {
    Write-Host ""
    Write-Host "Release $Version is ready locally." -ForegroundColor Green
    Write-Host "Push when ready:" -ForegroundColor Green
    Write-Host "  git push origin main"
    Write-Host "  git push origin $tagName"
} elseif ($PublishCrates) {
    Write-Host ""
    Write-Host "Release $Version is published and pushed." -ForegroundColor Green
    Write-Host "Published crates:" -ForegroundColor Green
    if ($PublishVoiceCrate) {
        Write-Host "  hematite-kokoros"
    }
    Write-Host "  hematite-cli"
}