feat(deploy): install to the fixed path again (one TCC grant, verified rollback)

Versioned filenames fixed the per-path code-signature cache kill (exit 137)
but made macOS TCC treat every build as a new app: each deploy re-prompted for
Documents/media-library access, and while the prompt was unanswered every
process touching those folders (model probes, session startup) blocked -
visible as slow/timeout requests in the app.

Go back to the fixed path ~/.hapi/bin/hapi (a real file) so TCC asks once and
remembers it across deploys. Guard the signature-cache hazard with explicit
verification: back up the installed binary to ~/.hapi/bin/backups/, install
with a fresh mtime, prove it execs repeatedly (--version x3) plus
codesign --verify, restart the hub / kickstart the runner, and restore the
backup on any failure.

- deploy-local.sh / deploy-remote.sh: fixed-path install, verify, rollback
- prune-backups.sh replaces prune-versions.sh (keeps newest N backups, drops
  legacy versioned binaries)
- AGENTS.md, docs/local-deployment.md and the hapi-deploy skill updated
This commit is contained in:
2026-09-17 09:38:08 +08:00
parent 14bbb3e8f8
commit bcfc69ed8c
6 changed files with 263 additions and 215 deletions
+32 -23
View File
@@ -76,26 +76,35 @@ cd android && ./gradlew :core:protocol:test # Android protocol conformance
## Local binary deployment
Deploy with `scripts/deploy-local.sh [tag]` after `bun run build:single-exe`.
The script signs with a pinned codesigning identity, copies to a new versioned
filename, repoints the `~/.hapi/bin/hapi` symlink, restarts the hub, refreshes
a running runner, and rolls back if `/health` fails.
Deploy with `scripts/deploy-local.sh [tag]` after `bun run build:single-exe`;
remote Macs: `scripts/deploy-remote.sh <ssh-target> [tag]`.
Two macOS rules the script enforces:
The binary always installs to the **fixed path** `~/.hapi/bin/hapi` (a real
file, no versioned filenames). Fixed path is deliberate: macOS TCC keys
permission grants (Documents, media library, ...) to the executable path, so
one stable path means the user grants access once and macOS remembers it
across deploys. Versioned filenames re-prompted for every build - do not
reintroduce them.
1. **Never overwrite `~/.hapi/bin/hapi` in place**, even with an atomic
rename. macOS caches the Mach-O code signature by executable path/mtime;
replacing that path can make the embedded signature disagree with the
cached signature and kill every new process with `OS_REASON_CODESIGNING` /
`embedded signature doesn't match attached signature` (often exit 137). A
plain `cp` also loses the signed mtime. Always copy to a new versioned
filename, keep the stable path as a symlink, and keep the previous
versioned file for rollback (do not delete it while sessions are running).
2. **Never ship an ad-hoc signature** (`--sign -`). Ad-hoc signatures have no
stable identity, so macOS TCC treats every build as a new app and re-asks
each protected permission (Documents, Downloads, media library, ...). Pin
one Apple Development identity instead: SHA-1 in `~/.hapi/signing-identity`,
override via `HAPI_SIGN_IDENTITY`, signed identifier `run.hapi.cli`.
The fixed path has one hazard: the kernel caches the Mach-O signature per
path, so overwriting it can kill new processes with
`OS_REASON_CODESIGNING` / `embedded signature doesn't match attached
signature` (often exit 137). The deploy scripts handle it:
1. back up the installed binary to `~/.hapi/bin/backups/` (newest
`HAPI_KEEP_BACKUPS` kept, default 2; never exec from the backup dir),
2. install the new file with a fresh mtime so the path-keyed signature cache
re-reads it,
3. prove it execs repeatedly (`hapi --version` x3) and `codesign --verify`,
4. restart the hub (local) / kickstart the launchd job (remote) and verify
(`/health` locally; runner state + exec path on the remote),
5. on any failure restore the backup onto the fixed path and restart.
Never leave an unverified binary installed. Never ship an ad-hoc signature
(`--sign -`): ad-hoc signatures have no stable identity, so macOS TCC treats
every build as a new app and re-asks each protected permission. Pin one Apple
Development identity instead: SHA-1 in `~/.hapi/signing-identity`, override
via `HAPI_SIGN_IDENTITY`, signed identifier `run.hapi.cli`.
The runner must be refreshed on every deploy (`hapi runner start` replaces the
stale one; running sessions survive). Compiled binaries never self-update: the
@@ -104,9 +113,9 @@ fixed for the life of the process. A stale runner keeps old machine RPCs and
capability flags, so hub features can fail with "restart the runner" errors.
Remote Macs with the same layout: `scripts/deploy-remote.sh <ssh-target> [tag]`
(signs locally, copies to a new versioned file, swaps the symlink, restarts the
launchd job; label defaults to `com.hapi.runner`, override with
`HAPI_REMOTE_LAUNCHD_LABEL`).
(signs locally, uploads next to the fixed path, installs by move, verifies,
then kickstarts the launchd job; label defaults to `com.hapi.runner`, override
with `HAPI_REMOTE_LAUNCHD_LABEL`).
Agent sessions must not run recursive `$HOME` sweeps (`find ~`, `du -sh ~`)
without pruning TCC-protected folders (`~/Music`, `~/Pictures`, `~/Movies`,
@@ -114,8 +123,8 @@ without pruning TCC-protected folders (`~/Music`, `~/Pictures`, `~/Movies`,
Apple Music prompt attributed to the hapi binary.
After deploy: `curl -fsS http://127.0.0.1:3006/health`, `~/.hapi/bin/hapi
--version`, `hapi runner status`. If health fails, restore the previous
symlink before doing anything else.
--version`, `hapi runner status`. If health fails, restore the backup from
`~/.hapi/bin/backups/` before doing anything else.
`docs/local-deployment.md` contains the same rationale and rollback checklist.
+72 -67
View File
@@ -46,39 +46,60 @@ bun run build:single-exe
scripts/deploy-local.sh [tag]
```
The script implements the sequence below, including the runner refresh; read
on to understand the macOS constraints it works around.
Remote Macs with the same layout:
### Code-signature cache (never overwrite the stable path)
```bash
scripts/deploy-remote.sh <ssh-target> [tag] # e.g. scripts/deploy-remote.sh k2lab card-dedupe
```
macOS caches the Mach-O signature against the executable pathname and
modification time. Replacing `~/.hapi/bin/hapi` in place (including a
temp-file + rename) or using a plain `cp` can leave a stale code-signature
cache. The next launch then fails with:
### Fixed install path (why)
The binary always installs to the **fixed path** `~/.hapi/bin/hapi` (a real
file). macOS TCC keys permission grants (Documents, Downloads, Apple
Music/media library, ...) to the executable path, so one stable path means the
user grants access **once** and macOS remembers it across deploys. The earlier
"new versioned filename per build + symlink" scheme re-prompted for every
build: each deploy looked like a brand-new app, and while the prompt was
unanswered every process touching a protected folder (model probes, session
startup) blocked - which showed up as slow/timeout requests in the app. Do not
reintroduce versioned filenames.
### Code-signature cache (why deploys verify, and roll back)
The fixed path has one hazard: the kernel caches the Mach-O signature per
path, so overwriting it can kill new processes with:
```text
OS_REASON_CODESIGNING
embedded signature doesn't match attached signature
```
This is a deployment/install issue, not a HAPI application error. Use a fresh
versioned path for every build and keep the stable command path as a symlink.
Do not deploy by copying over the stable path.
This is a deployment/install issue, not a HAPI application error. The deploy
scripts handle it end to end:
### Sign with a stable identity (TCC)
1. back up the installed binary to `~/.hapi/bin/backups/` (newest
`HAPI_KEEP_BACKUPS` kept, default 2; never exec from the backup dir)
2. install the new file with a fresh mtime so the path-keyed signature cache
re-reads it
3. prove it execs repeatedly (`hapi --version` x3) and `codesign --verify`
4. restart the hub (local) / kickstart the launchd job (remote) and verify
(`/health` locally; runner state + exec path on the remote)
5. on any failure restore the backup onto the fixed path and restart
macOS TCC records permission grants against the code-signing identity. An
ad-hoc signature (`codesign --sign -`) has no stable identity, so each rebuild
looks like a brand-new app and every protected permission (Documents,
Downloads, Apple Music/media library, ...) is asked again. Sign every build
with one pinned identity:
Never leave an unverified binary installed.
- `scripts/deploy-local.sh` stores the chosen Apple Development SHA-1 in
`~/.hapi/signing-identity` and reuses it. Override with
`HAPI_SIGN_IDENTITY` when rotating certificates.
- All builds use the signed identifier `run.hapi.cli`.
- With no Apple Development identity the script falls back to ad-hoc; that
still works, but expect TCC prompts to reappear after every deploy.
### Signing
Sign every build with one pinned identity; never ship an ad-hoc signature
(`codesign --sign -`), which has no stable identity and makes TCC re-ask every
permission:
- `scripts/sign-build.sh` (called by both deploy scripts) stores the chosen
Apple Development SHA-1 in `~/.hapi/signing-identity` and reuses it; override
with `HAPI_SIGN_IDENTITY` when rotating certificates
- all builds use the signed identifier `run.hapi.cli`
- with no Apple Development identity the scripts fall back to ad-hoc; that
still runs, but expect TCC prompts to reappear after every deploy
Agent sessions should also avoid recursive `$HOME` sweeps (`find ~`,
`du -sh ~`, ...) unless they prune TCC-protected folders (`~/Music`,
@@ -99,74 +120,58 @@ runner's own code stays old until the process restarts:
`scripts/deploy-local.sh` runs `hapi runner start` when a runner is already
running; the CLI stops the stale runner and starts a fresh one with
`HAPI_CLI_EXECUTABLE` pinned to the stable symlink. Running sessions are
detached and survive the restart.
`HAPI_CLI_EXECUTABLE` pinned to the fixed path. Running sessions are detached
and survive the restart.
### Manual sequence
```bash
set -euo pipefail
build=cli/dist-exe/bun-darwin-arm64/hapi
bin_dir="$HOME/.hapi/bin"
stable="$bin_dir/hapi"
stamp=$(date +%Y%m%d-%H%M%S)
release="$HOME/.hapi/bin/hapi.$stamp"
identity=$(cat "$HOME/.hapi/signing-identity") # SHA-1, or a unique cert name
# Sign with the pinned identity (see scripts/sign-build.sh).
bash scripts/sign-build.sh "$build"
# Bun's linker signature is not suitable after installation; re-sign once.
codesign --remove-signature "$build" 2>/dev/null || true
codesign --force --sign "$identity" --identifier run.hapi.cli "$build"
codesign --verify --deep --strict "$build"
# Back up the installed binary for rollback.
mkdir -p "$bin_dir/backups"
backup="$bin_dir/backups/hapi.$stamp"
[ -f "$stable" ] && cp -p "$stable" "$backup"
# -p preserves the mtime covered by the code-signature cache.
cp -p "$build" "$release"
codesign --verify --deep --strict "$release"
"$release" --version
# Keep the old target as a rollback point. If hapi is already a symlink,
# replace only the link; otherwise move the legacy regular file aside first.
stable="$HOME/.hapi/bin/hapi"
if [ -L "$stable" ]; then
old_target=$(readlink "$stable")
else
old_target="hapi.bak.$stamp"
mv "$stable" "$HOME/.hapi/bin/$old_target"
fi
ln -sfn "$(basename "$release")" "$stable"
# Install to the fixed path with a fresh mtime, then prove it execs.
rm -f "$stable"
cp "$build" "$stable"
chmod 755 "$stable"
"$stable" --version && "$stable" --version && "$stable" --version
codesign --verify --deep --strict "$stable"
launchctl kickstart -k "gui/$(id -u)/com.hapi.hub"
sleep 2
curl -fsS http://127.0.0.1:3006/health >/dev/null
"$stable" --version
# Refresh a running runner; new sessions already use the new binary via the
# symlink, but the runner's own machine RPCs/capabilities stay stale.
# Refresh a running runner so its machine RPCs/capabilities match.
HAPI_CLI_EXECUTABLE="$stable" "$stable" runner start
```
If the health check fails, immediately restore the prior link and restart the
agent:
If the health check or a cold start fails, restore the backup and restart:
```bash
ln -sfn "$old_target" "$HOME/.hapi/bin/hapi"
rm -f "$stable"
cp "$backup" "$stable"
chmod 755 "$stable"
"$stable" --version
launchctl kickstart -k "gui/$(id -u)/com.hapi.hub"
```
Never use `cp`, `mv`, or `codesign` on the stable symlink target after the
launch agent has been started. Keep versioned binaries until the replacement
has been running and verified.
### Remote machines
Macs that run the same layout (versioned binary + `~/.hapi/bin/hapi` symlink +
a supervised runner) are updated from here over SSH:
```bash
scripts/deploy-remote.sh <ssh-target> [tag] # e.g. scripts/deploy-remote.sh k2lab stable-signing
```
The script signs the build with the same pinned identity, copies it to a new
versioned file, swaps the symlink, restarts the launchd job (`com.hapi.runner`
by default; override with `HAPI_REMOTE_LAUNCHD_LABEL`), and verifies the runner
executes the new file. Running sessions survive; roll back by restoring the
previous symlink and kicking the job again. The remote host needs no build
toolchain — it only receives the signed binary.
`scripts/deploy-remote.sh <ssh-target> [tag]` performs the same flow over
SSH: signs locally, checks the remote arch matches, uploads next to the fixed
path, installs by move (fresh inode + mtime), proves the binary execs, then
kickstarts the launchd job (`com.hapi.runner` by default; override with
`HAPI_REMOTE_LAUNCHD_LABEL`) and verifies the runner executes the fixed path.
On any failure it restores the backup and kicks the job again. Running
sessions survive; the remote host needs no build toolchain.
+50 -30
View File
@@ -1,6 +1,15 @@
#!/bin/bash
# Deploy the freshly built all-in-one binary following docs/local-deployment.md.
# Deploy the freshly built all-in-one binary to the fixed path ~/.hapi/bin/hapi.
# Usage: scripts/deploy-local.sh [tag]
#
# Fixed path is deliberate: macOS TCC keys permission grants to the executable
# path, so a stable path means Documents / media-library access is granted once
# and remembered across deploys (versioned filenames re-prompted every build).
#
# At the same time macOS caches the Mach-O signature per path, so overwriting
# the path can kill new processes (exit 137). This script therefore installs
# with a fresh mtime, proves the new binary execs repeatedly, and rolls back
# from ~/.hapi/bin/backups on any failure.
set -euo pipefail
cd "$(dirname "$0")/.."
@@ -8,55 +17,64 @@ cd "$(dirname "$0")/.."
build=cli/dist-exe/bun-darwin-arm64/hapi
bin_dir="$HOME/.hapi/bin"
stable="$bin_dir/hapi"
backup_dir="$bin_dir/backups"
stamp=$(date +%Y%m%d-%H%M%S)
tag=${1:-}
release="$bin_dir/hapi.$stamp${tag:+-$tag}"
if [ ! -x "$build" ]; then
echo "error: build missing at $build; run 'bun run build:single-exe' first" >&2
exit 1
fi
mkdir -p "$bin_dir"
mkdir -p "$bin_dir" "$backup_dir"
bash scripts/sign-build.sh "$build"
# -p preserves the mtime covered by the code-signature cache.
cp -p "$build" "$release"
codesign --verify --deep --strict "$release"
"$release" --version
if [ -L "$stable" ]; then
old_target=$(readlink "$stable")
elif [ -e "$stable" ]; then
old_target="hapi.bak.$stamp"
mv "$stable" "$bin_dir/$old_target"
else
old_target=""
# Rollback copy of the currently installed binary. Never exec from here;
# rollback restores it onto the fixed path instead.
backup=""
if [ -f "$stable" ]; then
backup="$backup_dir/hapi.$stamp${tag:+-$tag}"
cp -p "$stable" "$backup"
echo "backup: $backup"
fi
ln -sfn "$(basename "$release")" "$stable"
echo "stable link: $stable -> $(readlink "$stable")"
echo "rollback target: ${old_target:-none}"
restore_backup() {
if [ -z "$backup" ]; then
echo "no backup to restore" >&2
return 1
fi
echo "restoring $backup" >&2
rm -f "$stable"
cp "$backup" "$stable"
chmod 755 "$stable"
"$stable" --version
}
# Install to the fixed path with a fresh mtime (invalidates the path-keyed
# signature cache), then prove it execs repeatedly.
rm -f "$stable"
cp "$build" "$stable"
chmod 755 "$stable"
"$stable" --version
"$stable" --version
"$stable" --version
codesign --verify --deep --strict "$stable"
echo "installed: $stable"
launchctl kickstart -k "gui/$(id -u)/com.hapi.hub"
sleep 2
if ! curl -fsS http://127.0.0.1:3006/health >/dev/null; then
echo "health check failed; restoring ${old_target:-none}" >&2
if [ -n "$old_target" ]; then
ln -sfn "$old_target" "$stable"
else
rm -f "$stable"
fi
echo "health check failed" >&2
restore_backup || true
launchctl kickstart -k "gui/$(id -u)/com.hapi.hub"
exit 1
fi
echo "health ok"
"$stable" --version
# Refresh the runner so its machine RPCs/capabilities match the new binary.
# Compiled binaries never self-update after a deploy: the heartbeat mtime check
# compares against the runner's own resolved exec path, which never changes.
# `runner start` stops the stale runner first; running sessions are unaffected.
# Compiled binaries never self-update: the heartbeat mtime check compares the
# runner's own resolved exec path, which is fixed for the life of the process.
runner_state="$HOME/.hapi/runner.state.json"
runner_pid=$(sed -n 's/.*"pid": *\([0-9][0-9]*\).*/\1/p' "$runner_state" 2>/dev/null | head -n 1 || true)
if [ -n "${runner_pid:-}" ] && kill -0 "$runner_pid" 2>/dev/null; then
@@ -70,5 +88,7 @@ else
echo "runner not running; skipped refresh"
fi
# Keep disk usage bounded: current + previous version (override HAPI_KEEP_VERSIONS).
bash scripts/prune-versions.sh "${HAPI_KEEP_VERSIONS:-2}" "$bin_dir"
# Keep disk usage bounded: newest N backups (default 2), drop legacy versioned files.
bash scripts/prune-backups.sh "${HAPI_KEEP_BACKUPS:-2}" "$bin_dir"
echo "rollback: rm -f '$stable' && cp '${backup:-<backup>}' '$stable' && chmod 755 '$stable' && launchctl kickstart -k gui/$(id -u)/com.hapi.hub"
+56 -34
View File
@@ -1,12 +1,16 @@
#!/bin/bash
# Deploy the built-and-signed all-in-one binary to a remote Mac over SSH.
# Usage: scripts/deploy-remote.sh <ssh-target> [tag]
# e.g. scripts/deploy-remote.sh k2lab stable-signing
# e.g. scripts/deploy-remote.sh k2lab card-dedupe
#
# The remote host must use this repo's deployment layout: a versioned binary
# under ~/.hapi/bin/ with the stable `hapi` symlink, supervised by a launchd
# job (default com.hapi.runner; override with HAPI_REMOTE_LAUNCHD_LABEL).
# Running sessions survive the restart; new sessions use the new binary.
# The remote host uses the same fixed-path layout as this repo:
# ~/.hapi/bin/hapi (real file) supervised by a launchd job
# (default com.hapi.runner; override with HAPI_REMOTE_LAUNCHD_LABEL).
#
# Fixed path keeps macOS TCC grants stable (granted once, remembered across
# deploys). To stay safe against the per-path code-signature cache the script
# backs up the installed binary, installs with a fresh mtime, proves the new
# binary execs repeatedly, and rolls back on any failure.
set -euo pipefail
cd "$(dirname "$0")/.."
@@ -21,7 +25,6 @@ fi
label=${HAPI_REMOTE_LAUNCHD_LABEL:-com.hapi.runner}
build=cli/dist-exe/bun-darwin-arm64/hapi
stamp=$(date +%Y%m%d-%H%M%S)
release="hapi.$stamp${tag:+-$tag}"
ssh_opts=(-o BatchMode=yes -o ConnectTimeout=10)
if [ ! -x "$build" ]; then
@@ -29,9 +32,7 @@ if [ ! -x "$build" ]; then
exit 1
fi
# Sign locally with the pinned identity: the remote TCC database keys grants
# to the signing identity, so shipping an ad-hoc build there re-triggers
# permission prompts after every deploy.
# Sign locally with the pinned identity so the remote keeps a stable signer.
bash scripts/sign-build.sh "$build"
local_arch=$(uname -m)
@@ -43,40 +44,59 @@ fi
remote_home=$(ssh "${ssh_opts[@]}" "$target" 'printf %s "$HOME"')
echo "target: $target ($remote_arch)"
echo "release: $release"
echo "release: $stamp${tag:+-$tag}"
# Copy to a NEW versioned filename: macOS caches Mach-O signatures by path,
# so never overwrite an existing executable path.
ssh "${ssh_opts[@]}" "$target" "mkdir -p '$remote_home/.hapi/bin'"
scp -q -C "${ssh_opts[@]}" "$build" "$target:$remote_home/.hapi/bin/$release"
# Upload next to the fixed path, then install by move (fresh inode + mtime).
ssh "${ssh_opts[@]}" "$target" "mkdir -p '$remote_home/.hapi/bin/backups'"
scp -q -C "${ssh_opts[@]}" "$build" "$target:$remote_home/.hapi/bin/.hapi.incoming"
ssh "${ssh_opts[@]}" "$target" "STAMP='$stamp' TAG='$tag' LABEL='$label' bash -s" <<'REMOTE'
set -euo pipefail
bin_dir="$HOME/.hapi/bin"
stable="$bin_dir/hapi"
new_name="hapi.$STAMP${TAG:+-$TAG}"
new="$bin_dir/$new_name"
backup_dir="$bin_dir/backups"
incoming="$bin_dir/.hapi.incoming"
if [ ! -x "$new" ]; then
echo "error: uploaded binary missing at $new" >&2
if [ ! -x "$incoming" ]; then
echo "error: uploaded binary missing at $incoming" >&2
exit 1
fi
codesign --verify --deep --strict "$new"
"$new" --version
backup=""
if [ -f "$stable" ]; then
backup="$backup_dir/hapi.$STAMP${TAG:+-$TAG}"
cp -p "$stable" "$backup"
echo "backup: $backup"
fi
restore_backup() {
if [ -z "$backup" ]; then
echo "no backup to restore" >&2
return 1
fi
echo "restoring $backup" >&2
rm -f "$stable"
cp "$backup" "$stable"
chmod 755 "$stable"
"$stable" --version
}
rm -f "$stable"
mv "$incoming" "$stable"
chmod 755 "$stable"
"$stable" --version
"$stable" --version
"$stable" --version
codesign --verify --deep --strict "$stable"
echo "installed: $stable"
if ! launchctl print "gui/$(id -u)/$LABEL" >/dev/null 2>&1; then
echo "error: launchd job $LABEL is not loaded on this host" >&2
restore_backup || true
exit 1
fi
old_target=""
if [ -L "$stable" ]; then
old_target=$(readlink "$stable")
fi
ln -sfn "$new_name" "$stable"
launchctl kickstart -k "gui/$(id -u)/$LABEL"
runner_pid=""
@@ -93,21 +113,23 @@ for _ in $(seq 1 15); do
runner_pid=""
done
echo "link: $(readlink "$stable")"
echo "rollback target: ${old_target:-none}"
if [ -z "$runner_pid" ]; then
echo "warning: runner not up yet; check 'launchctl print gui/$(id -u)/$LABEL'" >&2
echo "warning: runner did not come up" >&2
if restore_backup; then
launchctl kickstart -k "gui/$(id -u)/$LABEL"
fi
exit 1
fi
echo "runner pid: $runner_pid"
echo "runner exe: $runner_exe"
case "$runner_exe" in
*"$new_name"*) echo "remote deploy ok" ;;
*) echo "warning: runner executable is not $new_name; it will switch on the next restart" >&2 ;;
"$stable") echo "remote deploy ok" ;;
*) echo "warning: runner executable is $runner_exe, expected $stable" >&2 ;;
esac
REMOTE
# Keep disk usage bounded on the remote host: current + previous version.
ssh "${ssh_opts[@]}" "$target" "HAPI_KEEP_VERSIONS='${HAPI_KEEP_VERSIONS:-2}' bash -s" < scripts/prune-versions.sh
# Keep remote disk usage bounded: newest N backups, drop legacy versioned files.
ssh "${ssh_opts[@]}" "$target" "HAPI_KEEP_BACKUPS='${HAPI_KEEP_BACKUPS:-2}' bash -s" < scripts/prune-backups.sh
echo "rollback: ssh $target \"ln -sfn <old target> ~/.hapi/bin/hapi && launchctl kickstart -k gui/\\\$(id -u)/$label\""
echo "rollback: ssh $target \"rm -f ~/.hapi/bin/hapi && cp <backup> ~/.hapi/bin/hapi && chmod 755 ~/.hapi/bin/hapi && launchctl kickstart -k gui/\\\$(id -u)/$label\""
+53
View File
@@ -0,0 +1,53 @@
#!/bin/bash
# Keep disk usage bounded for the fixed-path deployment:
# - keep the newest N backup copies in <bin-dir>/backups (default 2)
# - remove legacy versioned binaries (hapi.YYYYMMDD-HHMMSS*) from the
# pre-fixed-path era; the fixed path ~/.hapi/bin/hapi is never touched
# Usage: scripts/prune-backups.sh [keep] [bin-dir]
set -euo pipefail
keep=${1:-${HAPI_KEEP_BACKUPS:-2}}
bin_dir=${2:-$HOME/.hapi/bin}
backup_dir="$bin_dir/backups"
case "$keep" in
''|*[!0-9]*)
echo "error: keep must be a number >= 1" >&2
exit 1
;;
esac
if [ "$keep" -lt 1 ]; then
echo "error: keep must be >= 1" >&2
exit 1
fi
if [ ! -d "$bin_dir" ]; then
echo "prune: no $bin_dir, nothing to do"
exit 0
fi
freed=0
if [ -d "$backup_dir" ]; then
count=0
for backup in $(ls -1 "$backup_dir" 2>/dev/null | grep -E '^hapi\.' | sort -r || true); do
count=$((count + 1))
if [ "$count" -le "$keep" ]; then
continue
fi
size=$(stat -f "%z" "$backup_dir/$backup" 2>/dev/null || echo 0)
rm -f "$backup_dir/$backup"
freed=$((freed + size))
echo "pruned backup: $backup ($((size / 1048576))MB)"
done
echo "kept backups: $((count < keep ? count : keep))"
fi
for version in $(ls -1 "$bin_dir" 2>/dev/null | grep -E '^hapi\.[0-9]{8}-[0-9]{6}' | sort -r || true); do
size=$(stat -f "%z" "$bin_dir/$version" 2>/dev/null || echo 0)
rm -f "$bin_dir/$version"
freed=$((freed + size))
echo "removed legacy: $version ($((size / 1048576))MB)"
done
echo "freed: $((freed / 1048576))MB"
-61
View File
@@ -1,61 +0,0 @@
#!/bin/bash
# Prune old versioned hapi binaries in a deployment bin dir, keeping the current
# symlink target plus the N most recent previous versions (default 2 total).
# Usage: scripts/prune-versions.sh [keep-count] [bin-dir]
# keep-count defaults to $HAPI_KEEP_VERSIONS, then 2 (current + previous).
# bin-dir defaults to $HOME/.hapi/bin.
# Safe to run while sessions are live: unlinking a binary does not affect
# running processes (they keep the open file), only future rollbacks.
set -euo pipefail
keep=${1:-${HAPI_KEEP_VERSIONS:-2}}
bin_dir=${2:-$HOME/.hapi/bin}
case "$keep" in
''|*[!0-9]*)
echo "error: keep-count must be a number >= 2" >&2
exit 1
;;
esac
if [ "$keep" -lt 2 ]; then
echo "error: keep-count must be >= 2 (current + previous)" >&2
exit 1
fi
if [ ! -d "$bin_dir" ]; then
echo "prune: no $bin_dir, nothing to do"
exit 0
fi
stable="$bin_dir/hapi"
current=""
if [ -L "$stable" ]; then
current=$(readlink "$stable")
fi
# Newest-first by filename: hapi.YYYYMMDD-HHMMSS[-tag] sorts chronologically.
versions=$(ls -1 "$bin_dir" 2>/dev/null | grep -E '^hapi\.[0-9]{8}-[0-9]{6}' | sort -r || true)
kept=""
count=0
freed=0
if [ -n "$current" ] && [ -f "$bin_dir/$current" ]; then
kept="$current"
count=1
fi
for version in $versions; do
[ "$version" = "$current" ] && continue
if [ "$count" -lt "$keep" ]; then
kept="$kept $version"
count=$((count + 1))
continue
fi
size=$(stat -f "%z" "$bin_dir/$version" 2>/dev/null || echo 0)
rm -f "$bin_dir/$version"
freed=$((freed + size))
echo "pruned: $version ($((size / 1048576))MB)"
done
echo "kept:${kept:- none} (count=$count)"
echo "freed: $((freed / 1048576))MB"