312 lines
12 KiB
TOML
312 lines
12 KiB
TOML
# Nix supplies the whole toolchain (Zig, GTK, libadwaita), so there is
|
|
# nothing for mise to install; these tasks just drive it.
|
|
|
|
[tasks.web-deps]
|
|
description = "Install the review UI's npm dependencies"
|
|
run = "nix develop --command npm --prefix web install --no-audit --no-fund"
|
|
|
|
[tasks.web-dev]
|
|
description = "Hot-reloading review UI against a running playpen (open /t/<tabId>/)"
|
|
run = "nix develop --command npm --prefix web run dev"
|
|
|
|
[tasks.build]
|
|
description = "Build a release binary into zig-out/bin"
|
|
run = "nix develop --command zig build -Doptimize=ReleaseFast"
|
|
|
|
[tasks.build-debug]
|
|
description = "Build a debug binary (leak checking, safety checks)"
|
|
run = "nix develop --command zig build"
|
|
|
|
[tasks.run]
|
|
description = "Build and run playpen"
|
|
run = "nix develop --command zig build run"
|
|
|
|
[tasks.fmt]
|
|
description = "Format all Zig source"
|
|
run = "nix develop --command zig fmt build.zig src"
|
|
|
|
[tasks.screenshot]
|
|
description = "Screenshot playpen in a throwaway headless compositor"
|
|
depends = ["build-debug"]
|
|
run = "nix develop --command ./shot.sh"
|
|
|
|
[tasks.install-claude-hooks]
|
|
description = "Teach Claude Code to report its state to the tab it runs in"
|
|
run = '''
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
command -v jq >/dev/null || { echo "install-claude-hooks needs jq" >&2; exit 1; }
|
|
|
|
claude="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
settings="$claude/settings.json"
|
|
script="$claude/hooks/playpen-status.sh"
|
|
|
|
mkdir -p "$claude/hooks"
|
|
install -m755 hooks/playpen-status.sh "$script"
|
|
|
|
# Copied rather than referenced in place: hooks configured to run out of a
|
|
# checkout break the day the checkout moves, and they break silently, in
|
|
# every Claude session at once.
|
|
[ -f "$settings" ] || echo '{}' > "$settings"
|
|
|
|
# Every entry carries a marker comment. It is what makes re-running this
|
|
# replace the previous install instead of stacking a second copy, and it is
|
|
# what `uninstall-claude-hooks` matches on. The shell ignores it.
|
|
merged=$(jq --arg script "$script" '
|
|
def marker: "# playpen-status-hook";
|
|
|
|
# Drop what a previous run added, leaving every other tools hooks alone.
|
|
def clean:
|
|
map(.hooks |= map(select((.command // "") | contains(marker) | not)))
|
|
| map(select((.hooks | length) > 0));
|
|
|
|
def entry($state; $async):
|
|
{ hooks: [
|
|
{ type: "command",
|
|
command: ($script + " " + $state + " " + marker),
|
|
timeout: 5 }
|
|
+ (if $async then { async: true } else {} end)
|
|
] };
|
|
|
|
.hooks //= {}
|
|
# UserPromptSubmit is the only one left synchronous: it reads the prompt off
|
|
# stdin to build the tab title. The rest are a single printf and need not
|
|
# hold the turn up at all.
|
|
| .hooks.UserPromptSubmit = ((.hooks.UserPromptSubmit // []) | clean) + [entry("busy"; false)]
|
|
| .hooks.Notification = ((.hooks.Notification // []) | clean) + [entry("input"; true)]
|
|
| .hooks.Stop = ((.hooks.Stop // []) | clean) + [entry("idle"; true)]
|
|
| .hooks.SessionStart = ((.hooks.SessionStart // []) | clean) + [entry("idle"; true)]
|
|
| .hooks.SessionEnd = ((.hooks.SessionEnd // []) | clean) + [entry("idle"; true)]
|
|
' "$settings")
|
|
|
|
# Keep a copy of what was there, and swap the new file in whole. Claude reads
|
|
# this file constantly; a half-written one is a broken session.
|
|
cp "$settings" "$settings.playpen-backup"
|
|
printf '%s\n' "$merged" > "$settings.tmp"
|
|
mv "$settings.tmp" "$settings"
|
|
|
|
echo "installed $script"
|
|
echo "hooked into $settings (previous version kept at $settings.playpen-backup)"
|
|
echo
|
|
echo "Open a new Claude session to pick them up — hooks are read at startup."
|
|
'''
|
|
|
|
[tasks.uninstall-claude-hooks]
|
|
description = "Remove the Claude Code hooks, leaving any other tools' alone"
|
|
run = '''
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
command -v jq >/dev/null || { echo "uninstall-claude-hooks needs jq" >&2; exit 1; }
|
|
|
|
claude="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
settings="$claude/settings.json"
|
|
[ -f "$settings" ] || { echo "no $settings; nothing to do"; exit 0; }
|
|
|
|
stripped=$(jq '
|
|
def marker: "# playpen-status-hook";
|
|
def clean:
|
|
map(.hooks |= map(select((.command // "") | contains(marker) | not)))
|
|
| map(select((.hooks | length) > 0));
|
|
|
|
.hooks //= {}
|
|
| reduce ("UserPromptSubmit","Notification","Stop","SessionStart","SessionEnd") as $ev
|
|
(.; .hooks[$ev] = ((.hooks[$ev] // []) | clean))
|
|
# An event we emptied was ours alone; drop the key rather than leave "Stop": []
|
|
| .hooks |= with_entries(select((.value | type) != "array" or (.value | length) > 0))
|
|
' "$settings")
|
|
|
|
cp "$settings" "$settings.playpen-backup"
|
|
printf '%s\n' "$stripped" > "$settings.tmp"
|
|
mv "$settings.tmp" "$settings"
|
|
|
|
rm -f "$claude/hooks/playpen-status.sh"
|
|
echo "removed the hooks from $settings (previous version kept at $settings.playpen-backup)"
|
|
'''
|
|
|
|
[tasks.install-skills]
|
|
description = "Install the review skills so Claude can drive the review pane"
|
|
run = '''
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
claude="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
dest="$claude/skills"
|
|
# Outside the skills directory on purpose: anything under it is loaded as a
|
|
# skill, so a backup kept alongside would show up in the list as a second,
|
|
# stale copy of the thing it is a backup of.
|
|
backups="$claude/playpen-skill-backups"
|
|
mkdir -p "$dest" "$backups"
|
|
|
|
for name in address-review leave-review; do
|
|
src=".claude/skills/$name"
|
|
[ -d "$src" ] || { echo "missing $src" >&2; exit 1; }
|
|
|
|
# Anything already installed under this name is kept, not replaced. These are
|
|
# global names: the version being overwritten may be one that drives an
|
|
# entirely different tool, and losing it silently would be the worst kind of
|
|
# breakage — the skill still runs, against a server that is not there.
|
|
if [ -e "$dest/$name" ] && [ ! -L "$dest/$name" ]; then
|
|
rm -rf "$backups/$name"
|
|
cp -r "$dest/$name" "$backups/$name"
|
|
echo "kept the previous $name at $backups/$name"
|
|
fi
|
|
|
|
rm -rf "$dest/$name"
|
|
cp -r "$src" "$dest/$name"
|
|
echo "installed $dest/$name"
|
|
done
|
|
|
|
echo
|
|
echo "Open a new Claude session to pick them up — skills are read at startup."
|
|
'''
|
|
|
|
[tasks.uninstall-skills]
|
|
description = "Remove the review skills, restoring whatever they replaced"
|
|
run = '''
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
claude="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
dest="$claude/skills"
|
|
backups="$claude/playpen-skill-backups"
|
|
|
|
for name in address-review leave-review; do
|
|
rm -rf "$dest/$name"
|
|
if [ -d "$backups/$name" ]; then
|
|
mv "$backups/$name" "$dest/$name"
|
|
echo "restored the previous $name from the backup"
|
|
else
|
|
echo "removed $dest/$name"
|
|
fi
|
|
done
|
|
rmdir "$backups" 2>/dev/null || true
|
|
'''
|
|
|
|
[tasks.install]
|
|
description = "Install playpen into ~/.local with a desktop entry and icon"
|
|
depends = ["build"]
|
|
run = '''
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
data="${XDG_DATA_HOME:-$HOME/.local/share}"
|
|
bindir="$HOME/.local/bin"
|
|
libexec="$HOME/.local/libexec/playpen"
|
|
apps="$data/applications"
|
|
icons="$data/icons/hicolor/scalable/apps"
|
|
gcroots="$data/playpen/gcroots"
|
|
|
|
# The binary carries an RPATH into the Nix store, so it runs anywhere, but it
|
|
# still has to be told where GTK's GSettings schemas and icon themes live or
|
|
# it aborts on a missing schema, where GIO's TLS module lives or every
|
|
# https:// page in a web pane fails, and which mesa to render web panes
|
|
# through or WebKit's web process dies on startup. Read those paths back out
|
|
# of the dev shell that built it rather than hardcoding store hashes.
|
|
schemas="$(nix develop --command printenv PLAYPEN_GSETTINGS_SCHEMA_DIR)"
|
|
datadirs="$(nix develop --command printenv PLAYPEN_RUNTIME_DATA_DIRS)"
|
|
giomodules="$(nix develop --command printenv PLAYPEN_GIO_MODULE_DIR)"
|
|
eglvendor="$(nix develop --command printenv PLAYPEN_EGL_VENDOR_DIR)"
|
|
dridrivers="$(nix develop --command printenv PLAYPEN_DRI_DRIVERS_PATH)"
|
|
gbmbackends="$(nix develop --command printenv PLAYPEN_GBM_BACKENDS_PATH)"
|
|
|
|
mkdir -p "$bindir" "$libexec" "$apps" "$icons" "$gcroots"
|
|
|
|
install -m755 zig-out/bin/playpen "$libexec/playpen"
|
|
|
|
# A launcher script rather than a symlink: a desktop launcher starts the app
|
|
# with a minimal environment, so the GTK runtime paths must be set here.
|
|
# Existing values are preserved so this composes with the rest of the session.
|
|
cat > "$bindir/playpen" <<EOF
|
|
#!/bin/sh
|
|
# Generated by \`mise run install\` — re-run that instead of editing.
|
|
export GSETTINGS_SCHEMA_DIR="$schemas\${GSETTINGS_SCHEMA_DIR:+:\$GSETTINGS_SCHEMA_DIR}"
|
|
export XDG_DATA_DIRS="$datadirs:\${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
|
|
export GIO_EXTRA_MODULES="$giomodules\${GIO_EXTRA_MODULES:+:\$GIO_EXTRA_MODULES}"
|
|
|
|
# Web panes render through the mesa this was built against. Mixing it with
|
|
# the host's mesa aborts WebKit's web process, so these are set outright
|
|
# rather than appended to whatever the session already had.
|
|
export __EGL_VENDOR_LIBRARY_DIRS="$eglvendor"
|
|
export LIBGL_DRIVERS_PATH="$dridrivers"
|
|
export GBM_BACKENDS_PATH="$gbmbackends"
|
|
|
|
# Warnings — a git invocation that timed out, a review server that could not
|
|
# listen — are the only account of why a review pane is behaving oddly, and a
|
|
# desktop launcher gives the app no terminal to print them to. Truncated per
|
|
# launch rather than appended, so this stays a log of the session you are in
|
|
# and never grows without bound.
|
|
log="\${XDG_STATE_HOME:-\$HOME/.local/state}/playpen"
|
|
mkdir -p "\$log"
|
|
exec "$libexec/playpen" "\$@" 2>"\$log/playpen.log"
|
|
EOF
|
|
chmod 755 "$bindir/playpen"
|
|
|
|
sed "s|@EXEC@|$bindir/playpen|" dist/dev.greyson.playpen.desktop.in \
|
|
> "$apps/dev.greyson.playpen.desktop"
|
|
install -m644 dist/dev.greyson.playpen.svg "$icons/dev.greyson.playpen.svg"
|
|
|
|
# Pin the store paths the installed binary links against. Without this a
|
|
# later `nix-collect-garbage` deletes GTK and the installed app stops
|
|
# starting, with no obvious connection to the command that broke it.
|
|
# Nix roots are transitive, so rooting each RPATH entry covers its closure.
|
|
rm -f "$gcroots"/*
|
|
{
|
|
# What the dynamic linker needs, read straight out of the binary.
|
|
if command -v readelf >/dev/null; then
|
|
readelf -d "$libexec/playpen" \
|
|
| sed -n 's/.*R\(UN\)\?PATH.*\[\(.*\)\]/\2/p' \
|
|
| tr ':' '\n'
|
|
else
|
|
echo "warning: readelf not found; only the launcher's own paths are pinned" >&2
|
|
fi
|
|
|
|
# What the launcher points at. These are opened by name at runtime rather
|
|
# than linked, so they never show up in the RPATH and would otherwise be
|
|
# left for the collector — losing the TLS module breaks https in web panes
|
|
# long after the command that deleted it.
|
|
printf '%s\n' "$schemas" "$datadirs" "$giomodules" \
|
|
"$eglvendor" "$dridrivers" "$gbmbackends" | tr ':' '\n'
|
|
} \
|
|
| sed -n 's|^\(/nix/store/[^/]*\).*|\1|p' \
|
|
| sort -u \
|
|
| while read -r path; do
|
|
nix-store --realise --add-root "$gcroots/$(basename "$path")" \
|
|
--indirect "$path" >/dev/null 2>&1 || true
|
|
done
|
|
echo "pinned $(find "$gcroots" -maxdepth 1 -type l | wc -l) store paths against garbage collection"
|
|
|
|
# Let the desktop notice the new entry and icon right away.
|
|
command -v update-desktop-database >/dev/null && update-desktop-database -q "$apps" || true
|
|
command -v gtk-update-icon-cache >/dev/null && gtk-update-icon-cache -qtf "$data/icons/hicolor" 2>/dev/null || true
|
|
|
|
echo "installed $bindir/playpen"
|
|
case ":$PATH:" in
|
|
*":$bindir:"*) ;;
|
|
*) echo "note: $bindir is not on your PATH; the desktop entry still works" >&2 ;;
|
|
esac
|
|
'''
|
|
|
|
[tasks.uninstall]
|
|
description = "Remove everything `install` put into ~/.local"
|
|
run = '''
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
data="${XDG_DATA_HOME:-$HOME/.local/share}"
|
|
apps="$data/applications"
|
|
|
|
rm -f "$HOME/.local/bin/playpen"
|
|
rm -rf "$HOME/.local/libexec/playpen"
|
|
rm -f "$apps/dev.greyson.playpen.desktop"
|
|
rm -f "$data/icons/hicolor/scalable/apps/dev.greyson.playpen.svg"
|
|
# Dropping the roots lets Nix reclaim the runtime closure on its next GC.
|
|
rm -rf "$data/playpen"
|
|
|
|
command -v update-desktop-database >/dev/null && update-desktop-database -q "$apps" || true
|
|
command -v gtk-update-icon-cache >/dev/null && gtk-update-icon-cache -qtf "$data/icons/hicolor" 2>/dev/null || true
|
|
|
|
echo "uninstalled playpen"
|
|
'''
|