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
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ jobs:
- run: cargo fmt --all --check
- run: cargo clippy --workspace --all-targets -- -D warnings
- run: cargo test --workspace
- run: python3 herdr/test-manifest.py
- run: cargo build --release -p plannotator-tui
- run: bash herdr/stage-plannotator-tui.sh
- run: cmp target/release/plannotator-tui herdr/bin/plannotator-tui.exe
- run: ./herdr/bin/plannotator-tui.exe --version

check-windows:
runs-on: windows-latest
Expand All @@ -41,3 +46,10 @@ jobs:
- run: cargo test --workspace --all-targets
- run: cargo build --release -p plannotator-tui
- run: '& .\target\release\plannotator-tui.exe --version'
- run: python herdr/test-manifest.py
- run: powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File herdr/stage-plannotator-tui.ps1
- run: |
$source = (Get-FileHash .\target\release\plannotator-tui.exe).Hash
$staged = (Get-FileHash .\herdr\bin\plannotator-tui.exe).Hash
if ($source -ne $staged) { throw "staged binary differs from Cargo output" }
- run: '& .\herdr\bin\plannotator-tui.exe --version'
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/target
/herdr/bin/
*.annotations.json.tmp
25 changes: 18 additions & 7 deletions herdr/herdr-plugin.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,32 @@ name = "plannotator-tui"
version = "0.2.0"
min_herdr_version = "0.8.0"
description = "Annotate Markdown in a pane: select text, comment, looks-good, delete; send feedback to the agent."
platforms = ["macos", "linux"]
platforms = ["macos", "linux", "windows"]

# Where plannotator-tui opens is the user's choice, not the manifest's: `[herdr] placement` in
# ~/.config/plannotator-tui/config.toml (overlay | split | popup), or PLANNOTATOR_TUI_PLACEMENT. The
# launcher (`plannotator-tui herdr open`) passes --placement, which overrides the default below.
# its config file (overlay | split | popup), or PLANNOTATOR_TUI_PLACEMENT. The launcher
# (`plannotator-tui herdr open`) passes --placement, which overrides the default below.
# overlay = a real pane zoomed over the whole tab; Herdr restores focus and zoom on exit.

[[build]]
command = ["cargo", "build", "--release", "--manifest-path", "../Cargo.toml"]

[[build]]
platforms = ["macos", "linux"]
command = ["bash", "stage-plannotator-tui.sh"]

[[build]]
platforms = ["windows"]
command = [
"powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
"-File", "stage-plannotator-tui.ps1",
]

[[panes]]
id = "doc"
title = "plannotator-tui"
placement = "overlay"
command = ["sh", "-c", "exec \"$HERDR_PLUGIN_ROOT/../target/release/plannotator-tui\" herdr pane"]
command = ["./bin/plannotator-tui.exe", "herdr", "pane"]

# Human entry points. Both run the launcher, which reads Herdr's invocation context: the
# focused pane's folder (open) or the clicked file:// link (open-link), and the focused
Expand All @@ -26,20 +37,20 @@ command = ["sh", "-c", "exec \"$HERDR_PLUGIN_ROOT/../target/release/plannotator-
id = "open"
title = "Annotate: open here"
contexts = ["workspace", "pane"]
command = ["sh", "-c", "exec \"$HERDR_PLUGIN_ROOT/../target/release/plannotator-tui\" herdr open"]
command = ["./bin/plannotator-tui.exe", "herdr", "open"]

[[actions]]
id = "open-link"
title = "Annotate this file"
contexts = ["pane"]
command = ["sh", "-c", "exec \"$HERDR_PLUGIN_ROOT/../target/release/plannotator-tui\" herdr open"]
command = ["./bin/plannotator-tui.exe", "herdr", "open"]

# Ctrl-click on a file:// markdown link. Anchored on the scheme so web links never match.
[[actions]]
id = "last"
title = "Annotate: agent's last message"
contexts = ["pane"]
command = ["sh", "-c", "exec \"$HERDR_PLUGIN_ROOT/../target/release/plannotator-tui\" herdr last"]
command = ["./bin/plannotator-tui.exe", "herdr", "last"]

[[link_handlers]]
id = "markdown-file"
Expand Down
10 changes: 10 additions & 0 deletions herdr/stage-plannotator-tui.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
$ErrorActionPreference = "Stop"

$pluginRoot = $PSScriptRoot
$repositoryRoot = Split-Path -Parent $pluginRoot
$source = Join-Path $repositoryRoot "target/release/plannotator-tui.exe"
$destinationDirectory = Join-Path $pluginRoot "bin"
$destination = Join-Path $destinationDirectory "plannotator-tui.exe"

New-Item -ItemType Directory -Force $destinationDirectory | Out-Null
Copy-Item -LiteralPath $source -Destination $destination -Force
12 changes: 12 additions & 0 deletions herdr/stage-plannotator-tui.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail

plugin_root="$(cd "$(dirname "$0")" && pwd)"
repository_root="$(cd "$plugin_root/.." && pwd)"

mkdir -p "$plugin_root/bin"
# Remove first: cp over an existing signed binary reuses the inode and invalidates the
# macOS code signature, and the next exec is killed.
rm -f "$plugin_root/bin/plannotator-tui.exe"
cp "$repository_root/target/release/plannotator-tui" \
"$plugin_root/bin/plannotator-tui.exe"
117 changes: 117 additions & 0 deletions herdr/test-manifest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#!/usr/bin/env python3
"""Check the Windows Full entrypoint contract across one or more manifests."""

from __future__ import annotations

import sys
import tomllib
from pathlib import Path


PROGRAM = "./bin/plannotator-tui.exe"
COMMANDS = {
("panes", "doc"): [PROGRAM, "herdr", "pane"],
("actions", "open"): [PROGRAM, "herdr", "open"],
("actions", "open-link"): [PROGRAM, "herdr", "open"],
("actions", "last"): [PROGRAM, "herdr", "last"],
}
LINK_HANDLERS = {"markdown-file": "open-link"}
DEVELOPMENT_BUILDS = [
["cargo", "build", "--release", "--manifest-path", "../Cargo.toml"],
["bash", "stage-plannotator-tui.sh"],
[
"powershell.exe",
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-File",
"stage-plannotator-tui.ps1",
],
]


def fail(path: Path, message: str) -> None:
raise AssertionError(f"{path}: {message}")


def supports_windows(manifest: dict[str, object], entry: dict[str, object]) -> bool:
platforms = entry.get("platforms", manifest.get("platforms", []))
return isinstance(platforms, list) and "windows" in platforms


def entry(
path: Path,
manifest: dict[str, object],
table: str,
entry_id: str,
) -> dict[str, object]:
entries = manifest.get(table, [])
if not isinstance(entries, list):
fail(path, f"[[{table}]] is not an array")
matches = [
item
for item in entries
if isinstance(item, dict)
and item.get("id") == entry_id
and supports_windows(manifest, item)
]
if len(matches) != 1:
fail(path, f"expected one Windows {table}.{entry_id}, found {len(matches)}")
return matches[0]


def shape(path: Path) -> dict[str, tuple[str, ...]]:
with path.open("rb") as handle:
manifest = tomllib.load(handle)

if set(manifest.get("platforms", [])) != {"macos", "linux", "windows"}:
fail(path, "top-level platforms must be macos, linux, and windows")

result: dict[str, tuple[str, ...]] = {}
for (table, entry_id), expected in COMMANDS.items():
command = entry(path, manifest, table, entry_id).get("command")
if command != expected:
fail(path, f"{table}.{entry_id} command is {command!r}, expected {expected!r}")
result[entry_id] = tuple(expected[1:])

for entry_id, expected_action in LINK_HANDLERS.items():
action = entry(path, manifest, "link_handlers", entry_id).get("action")
if action != expected_action:
fail(path, f"link_handlers.{entry_id} action is {action!r}, expected {expected_action!r}")
result[entry_id] = (expected_action,)

return result


def check_development_builds(path: Path) -> None:
with path.open("rb") as handle:
manifest = tomllib.load(handle)
builds = manifest.get("build", [])
if not isinstance(builds, list):
fail(path, "[[build]] is not an array")
commands = [item.get("command") for item in builds if isinstance(item, dict)]
if commands != DEVELOPMENT_BUILDS:
fail(path, f"development build commands are {commands!r}")
if not supports_windows(manifest, builds[0]):
fail(path, "Cargo build does not run on Windows")
if set(builds[1].get("platforms", [])) != {"macos", "linux"}:
fail(path, "Unix staging command must run on macOS and Linux only")
if set(builds[2].get("platforms", [])) != {"windows"}:
fail(path, "PowerShell staging command must run on Windows only")


def main() -> None:
paths = [Path(value) for value in sys.argv[1:]] or [
Path(__file__).with_name("herdr-plugin.toml")
]
reference = shape(paths[0])
check_development_builds(paths[0])
for path in paths[1:]:
candidate = shape(path)
if candidate != reference:
fail(path, f"Full entrypoint tails differ: {candidate!r} != {reference!r}")


if __name__ == "__main__":
main()
Loading