Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/windows-full-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ on:
- "scripts/test-fetch-herdr-annotate.*"
- "scripts/test-fetch-plannotator-tui.*"
- "scripts/test-herdr-windows-plugin.ps1"
- "scripts/test-herdr-windows-full-plugin.ps1"
- "scripts/test-herdr-windows-full-pane.ps1"
- "scripts/test-herdr-windows-full-paths.ps1"
- "scripts/test-http-server.py"
- "scripts/test-windows-full-manifest.py"
- "windows-full/**"
- ".github/workflows/windows-full-ci.yml"
merge_group:

Expand Down Expand Up @@ -53,6 +57,12 @@ jobs:
plannotator-tui-source/herdr/herdr-plugin.toml
- name: pinned Herdr link and list
run: ./scripts/test-herdr-windows-plugin.ps1
- name: pinned Herdr windows-full acceptance and 0.8.2 rejection
run: ./scripts/test-herdr-windows-full-plugin.ps1
- name: windows-full review pane render, quit and teardown
run: ./scripts/test-herdr-windows-full-pane.ps1
- name: windows-full path matrix
run: ./scripts/test-herdr-windows-full-paths.ps1

unix-regression:
strategy:
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
.DS_Store
bin/*
!bin/.gitkeep
windows-full/bin/*
!windows-full/bin/.gitkeep
/rust/target/
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,16 @@ There is no runtime to install. Both installs download a small prebuilt `herdr-a

On Linux, install `wl-clipboard`, `xclip`, or `xsel` for clipboard access.

On Windows, native Herdr plugin support is preview/best-effort. Clipboard access uses PowerShell; no extra clipboard package is required. The install, keybinding, configuration check, reload, and use instructions below also apply on Windows.
On Windows, native Herdr plugin support is preview/best-effort. Lite installs on Herdr 0.8.0 or later; Full installs through the Windows Full variant below, which needs Herdr 0.9.0 or later. Clipboard access uses PowerShell; no extra clipboard package is required. The install, keybinding, configuration check, reload, and use instructions below also apply on Windows.

## Install

Pick one. Installing the other later just swaps it (same plugin id). An install stays on the commit it came from; run the same command again to move to the current release.
Pick one. Installing another later just swaps it (same plugin id). An install stays on the commit it came from; run the same command again to move to the current release.

<img src="assets/install-full.svg" width="200" align="left" alt="Full">

**Full:** annotate terminal text, review documents and agent replies, send feedback to the agent.
Wraps [Plannotator TUI](https://github.com/plannotator/plannotator-tui) (macOS and Linux today). [Demo](https://x.com/plannotator/status/2093419561077154287)
Wraps [Plannotator TUI](https://github.com/plannotator/plannotator-tui) (macOS and Linux; Windows via the windows-full variant below). [Demo](https://x.com/plannotator/status/2093419561077154287)

```sh
herdr plugin install plannotator/herdr-annotate
Expand All @@ -50,6 +50,16 @@ herdr plugin install plannotator/herdr-annotate/lite

<br clear="all">

**Windows Full (preview):** the Full install on Windows, opt-in. Requires Herdr 0.9.0 or later on Windows.

```sh
herdr plugin install plannotator/herdr-annotate/windows-full
```

It stages `plannotator-tui` natively, so no launcher sits between Herdr and the TUI. It shares the same native runtime as the Lite and Full installs, so annotations and archives stay in one place.

This is a preview. The pane and path handling were verified on Windows 11 x86_64 ([PR #49](https://github.com/plannotator/herdr-annotate/pull/49), [issue #43](https://github.com/plannotator/herdr-annotate/issues/43)). Native ARM64 is untested. Report results on [issue #43](https://github.com/plannotator/herdr-annotate/issues/43).

> **Required.** Bind the keys in Herdr's config.

<details open>
Expand Down
33 changes: 21 additions & 12 deletions scripts/fetch-herdr-annotate.ps1
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
$ErrorActionPreference = "Stop"
Set-Location (Join-Path $PSScriptRoot "..")
# Anchored on the script's own location rather than the current directory, and every path
# below is absolute and literal. A checkout path may legitimately contain square brackets --
# the Windows acceptance path matrix requires it -- and PowerShell reads those as wildcards in
# any -Path parameter. Worse, once the current directory contains them, it is stored escaped,
# so even -LiteralPath with a relative path resolves to a name with backticks in it and is not
# found. Not depending on the current directory at all is what makes that whole class go away.
# .NET rather than Split-Path: an extended-length \\?\ root is a path matrix row, and
# Split-Path cannot parse one -- it reports a null drive and returns nothing.
$root = [System.IO.Path]::GetDirectoryName($PSScriptRoot)

$versionContents = Get-Content -LiteralPath "herdr-annotate.version" -Raw
$versionContents = Get-Content -LiteralPath ([System.IO.Path]::Combine($root, "herdr-annotate.version")) -Raw
$version = if ($null -eq $versionContents) { "" } else { [string]$versionContents }
$version = $version.Trim()
if (-not $version) { throw "herdr-annotate.version is empty" }
New-Item -ItemType Directory -Force "bin" | Out-Null
$destination = Join-Path "bin" "herdr-annotate.exe"
$stamp = Join-Path "bin" "herdr-annotate.version"
$binDirectory = [System.IO.Path]::Combine($root, "bin")
[System.IO.Directory]::CreateDirectory($binDirectory) | Out-Null
$destination = [System.IO.Path]::Combine($binDirectory, "herdr-annotate.exe")
$stamp = [System.IO.Path]::Combine($binDirectory, "herdr-annotate.version")
$installed = if (Test-Path -LiteralPath $stamp -PathType Leaf) { ([string](Get-Content -LiteralPath $stamp -Raw)).Trim() } else { "" }

if ((Test-Path $destination) -and $installed -eq $version -and -not $env:HERDR_ANNOTATE_BIN) {
if ((Test-Path -LiteralPath $destination) -and $installed -eq $version -and -not $env:HERDR_ANNOTATE_BIN) {
Write-Output "herdr-annotate $version already installed"
exit 0
}

if ($env:HERDR_ANNOTATE_BIN) {
if (-not (Test-Path $env:HERDR_ANNOTATE_BIN -PathType Leaf)) {
if (-not (Test-Path -LiteralPath $env:HERDR_ANNOTATE_BIN -PathType Leaf)) {
throw "HERDR_ANNOTATE_BIN is not a file: $env:HERDR_ANNOTATE_BIN"
}
Copy-Item -Force -LiteralPath $env:HERDR_ANNOTATE_BIN -Destination "$destination.tmp"
Expand All @@ -35,20 +44,20 @@ $target = switch ($architecture) {
$asset = "herdr-annotate-$target.exe"
$base = "https://github.com/plannotator/herdr-annotate/releases/download/rust-lite-v$version"
$temporary = Join-Path ([System.IO.Path]::GetTempPath()) ("herdr-annotate-" + [guid]::NewGuid())
New-Item -ItemType Directory $temporary | Out-Null
[System.IO.Directory]::CreateDirectory($temporary) | Out-Null
try {
Invoke-WebRequest -UseBasicParsing "$base/$asset" -OutFile (Join-Path $temporary $asset)
Invoke-WebRequest -UseBasicParsing "$base/SHA256SUMS" -OutFile (Join-Path $temporary "SHA256SUMS")
$line = Get-Content (Join-Path $temporary "SHA256SUMS") | Where-Object { $_ -match "\s$([regex]::Escape($asset))$" } | Select-Object -First 1
$line = Get-Content -LiteralPath (Join-Path $temporary "SHA256SUMS") | Where-Object { $_ -match "\s$([regex]::Escape($asset))$" } | Select-Object -First 1
if (-not $line) { throw "$asset is not listed in $base/SHA256SUMS" }
$expected = ($line -split "\s+")[0].ToLowerInvariant()
$actual = (Get-FileHash -Algorithm SHA256 (Join-Path $temporary $asset)).Hash.ToLowerInvariant()
$actual = (Get-FileHash -Algorithm SHA256 -LiteralPath (Join-Path $temporary $asset)).Hash.ToLowerInvariant()
if ($actual -ne $expected) { throw "sha256 mismatch for ${asset}: expected $expected, got $actual" }
Copy-Item -Force (Join-Path $temporary $asset) "$destination.tmp"
Copy-Item -Force -LiteralPath (Join-Path $temporary $asset) -Destination "$destination.tmp"
Move-Item -Force -LiteralPath "$destination.tmp" -Destination $destination
Set-Content -LiteralPath $stamp -NoNewline -Value $version
Write-Output "installed herdr-annotate $version ($target)"
}
finally {
Remove-Item -Recurse -Force $temporary -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force -LiteralPath $temporary -ErrorAction SilentlyContinue
}
59 changes: 46 additions & 13 deletions scripts/fetch-plannotator-tui.ps1
Original file line number Diff line number Diff line change
@@ -1,18 +1,42 @@
# -DestinationDirectory stages the binary and its stamp somewhere other than the repository's
# own bin/, which is how the windows-full variant keeps its copy beside its manifest. Omitted,
# the destination is unchanged, so every existing caller behaves exactly as before. The pin is
# read from the repository root either way: there is one release pin, not one per variant.
param([string]$DestinationDirectory)

$ErrorActionPreference = "Stop"
Set-Location (Join-Path $PSScriptRoot "..")
# Anchored on the script's own location rather than the current directory, and every path
# below is absolute and literal. A checkout path may legitimately contain square brackets --
# the Windows acceptance path matrix requires it -- and PowerShell reads those as wildcards in
# any -Path parameter. Worse, once the current directory contains them, it is stored escaped,
# so even -LiteralPath with a relative path resolves to a name with backticks in it and is not
# found. Not depending on the current directory at all is what makes that whole class go away.
# .NET rather than Split-Path: an extended-length \\?\ root is a path matrix row, and
# Split-Path cannot parse one -- it reports a null drive and returns nothing.
$root = [System.IO.Path]::GetDirectoryName($PSScriptRoot)

$versionContents = Get-Content -LiteralPath "plannotator-tui.version" -Raw
$versionContents = Get-Content -LiteralPath ([System.IO.Path]::Combine($root, "plannotator-tui.version")) -Raw
$version = if ($null -eq $versionContents) { "" } else { [string]$versionContents }
$version = $version.Trim()
if (-not $version) { throw "plannotator-tui.version is empty" }

$destinationDirectory = Join-Path (Get-Location).Path "bin"
$destination = Join-Path $destinationDirectory "plannotator-tui.exe"
$stamp = Join-Path $destinationDirectory "plannotator-tui.version"
New-Item -ItemType Directory -Force $destinationDirectory | Out-Null
$destinationDirectory = if ([string]::IsNullOrWhiteSpace($DestinationDirectory)) {
[System.IO.Path]::Combine($root, "bin")
} else {
$DestinationDirectory
}
$destination = [System.IO.Path]::Combine($destinationDirectory, "plannotator-tui.exe")
$stamp = [System.IO.Path]::Combine($destinationDirectory, "plannotator-tui.version")
# .NET rather than New-Item for the same reason: the destination is an absolute path that may
# contain brackets, and New-Item -Path would treat them as a wildcard.
[System.IO.Directory]::CreateDirectory($destinationDirectory) | Out-Null

$localOverride = [Environment]::GetEnvironmentVariable("PLANNOTATOR_TUI_BIN", "Process")
$hasLocalOverride = $null -ne $localOverride
# Empty counts as absent, as it does for PLANNOTATOR_TUI_RELEASE_BASE below and in the
# Unix fetcher: a caller clearing the name through an API that binds $null as
# "" leaves it defined, and an empty override would otherwise reach the "is not a file"
# throw, which sits outside the warn-and-exit-zero path and fails the build outright.
$hasLocalOverride = -not [string]::IsNullOrWhiteSpace($localOverride)
$installed = if (Test-Path -LiteralPath $stamp -PathType Leaf) {
([string](Get-Content -LiteralPath $stamp -Raw)).Trim()
} else {
Expand All @@ -28,9 +52,9 @@ if ((Test-Path -LiteralPath $destination -PathType Leaf) -and
function Install-PlannotatorTui {
param([Parameter(Mandatory = $true)][string]$Source)

$candidate = Join-Path $destinationDirectory ("plannotator-tui-" + [guid]::NewGuid() + ".tmp")
$backup = Join-Path $destinationDirectory ("plannotator-tui-" + [guid]::NewGuid() + ".bak")
$stampBackup = Join-Path $destinationDirectory ("plannotator-tui-version-" + [guid]::NewGuid() + ".bak")
$candidate = [System.IO.Path]::Combine($destinationDirectory, ("plannotator-tui-" + [guid]::NewGuid() + ".tmp"))
$backup = [System.IO.Path]::Combine($destinationDirectory, ("plannotator-tui-" + [guid]::NewGuid() + ".bak"))
$stampBackup = [System.IO.Path]::Combine($destinationDirectory, ("plannotator-tui-version-" + [guid]::NewGuid() + ".bak"))
$hadDestination = Test-Path -LiteralPath $destination -PathType Leaf
$hadStamp = Test-Path -LiteralPath $stamp -PathType Leaf
$replacementCompleted = $false
Expand Down Expand Up @@ -112,16 +136,20 @@ try {
"PLANNOTATOR_TUI_RELEASE_BASE",
"Process"
)
# PLANNOTATOR_TUI_RELEASE_BASE is a test-only seam for a loopback fixture server.
$base = if ($null -ne $releaseBaseOverride) {
# PLANNOTATOR_TUI_RELEASE_BASE is a test-only seam for a loopback fixture server. An empty
# value counts as absent: a caller clearing it through an API that binds $null as "" would
# otherwise leave the name defined, and an empty base builds a URL with no host at all --
# which surfaces as "invalid URI" long after the mistake, through the warn-and-exit-zero
# contract that hides it.
$base = if (-not [string]::IsNullOrWhiteSpace($releaseBaseOverride)) {
$releaseBaseOverride.TrimEnd([char]"/")
} else {
"https://github.com/plannotator/plannotator-tui/releases/download/v$version"
}

$temporary = Join-Path ([System.IO.Path]::GetTempPath()) ("plannotator-tui-" + [guid]::NewGuid())
try {
New-Item -ItemType Directory $temporary | Out-Null
[System.IO.Directory]::CreateDirectory($temporary) | Out-Null
$downloadedAsset = Join-Path $temporary $asset
$checksumFile = Join-Path $temporary "SHA256SUMS"
Invoke-WebRequest -UseBasicParsing "$base/$asset" -OutFile $downloadedAsset
Expand Down Expand Up @@ -155,3 +183,8 @@ try {
)
exit 0
}

# Explicit rather than falling off the end: the windows-full wrapper propagates
# $LASTEXITCODE, which a script ending without an exit leaves undefined or at whatever
# the last native command set.
exit 0
Loading
Loading