Files
pastilhas eec6a4962e install.sh: clone anywhere, run one command
A manifest declares repo path -> target, and the script walks it. Symlinks, not
copies, so an installed file edits the repo and one commit reaches every other
machine. Idempotent — a correct link is a no-op, safe after every pull. A real
file where a link should go is moved to .bak-<timestamp> and reported, because
on a new machine that may be the only copy of something written by hand.

Conditional targets are what make it portable: officer.dev/CLAUDE.md and the
officer-chat-events hook both name ~/officer-dev/platform, so a laptop with no
Officer install gets neither instead of getting a document that lies.

mask-secret is a command now rather than a path only true on this machine, and
user/CLAUDE.md says so. It is a generated wrapper, not a symlink, and that is
forced: mask-secret.py resolves its own path to find mask-hooks.d, so a symlink
would reach back into the repo and run every machine's hooks. The wrapper sets
MASK_HOOKS_DIR to the ones this machine installed and still runs the script from
the repo, so no code is copied.

Verified against a scratch HOME: re-run is all ok, the hand-written file
survives as .bak, and a machine without Officer skips both conditional lines.
2026-08-21 00:24:22 +00:00

100 lines
3.2 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Put every file in this repo where the tooling expects it, on any machine.
#
# git clone git@gitea.officer.dev:pastilhas/claude-mds.git ~/projects/claude-mds
# ~/projects/claude-mds/install.sh
#
# Symlinks, never copies. Two copies drift, and the point of the repo is that an edit lands in git.
# Safe to re-run after every pull: a correct link is a no-op. A real file in the way is backed up,
# never overwritten in silence.
#
# --dry-run say what would happen, change nothing
#
set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MANIFEST="$REPO/manifest"
CLAUDE_SCRIPTS="${CLAUDE_SCRIPTS:-$HOME/.local/claude-scripts}"
HOOKS_DIR="$CLAUDE_SCRIPTS/mask-hooks.d"
BIN_DIR="${BIN_DIR:-$HOME/.local/bin}"
STAMP="$(date +%Y%m%d-%H%M%S)"
DRY=0
[[ ${1:-} == --dry-run ]] && DRY=1
say() { printf ' %-7s %s\n' "$1" "$2"; }
run() { (( DRY )) || "$@"; }
# Symlink $2 -> $1, refusing to destroy anything that is not already our link.
link() {
local src=$1 dst=$2
if [[ ! -e $src ]]; then say "MISSING" "$src — not in the repo"; return 1; fi
if [[ -L $dst && "$(readlink -f "$dst")" == "$(readlink -f "$src")" ]]; then
say "ok" "$dst"; return 0
fi
# A real file, or a link somewhere else. It may be the only copy of something hand-written here.
if [[ -e $dst || -L $dst ]]; then
say "backup" "$dst -> $dst.bak-$STAMP"
run mv "$dst" "$dst.bak-$STAMP"
fi
run mkdir -p "$(dirname "$dst")"
run ln -sfn "$src" "$dst"
say "link" "$dst"
}
# `mask-secret` as a COMMAND, not a path — so user/CLAUDE.md can name it and be true everywhere.
#
# A wrapper rather than a symlink, and that is forced: mask-secret.py finds its hooks at
# `Path(__file__).resolve().parent/mask-hooks.d`, and .resolve() follows a symlink back into the repo
# — which holds EVERY machine's hooks. The wrapper points MASK_HOOKS_DIR at the ones this machine
# actually installed, and still executes the script from the repo, so the code is never copied.
install_command() {
local script="$REPO/tools/mask-secret.py"
[[ -e $script ]] || { say "MISSING" "$script"; return 1; }
run mkdir -p "$BIN_DIR" "$HOOKS_DIR"
if (( ! DRY )); then
cat > "$BIN_DIR/mask-secret" <<WRAPPER
#!/bin/sh
# Generated by claude-mds/install.sh — edit install.sh, not this.
export MASK_HOOKS_DIR="$HOOKS_DIR"
exec "$script" "\$@"
WRAPPER
chmod +x "$BIN_DIR/mask-secret"
fi
say "cmd" "$BIN_DIR/mask-secret"
}
main() {
(( DRY )) && echo "dry run — nothing will change"
echo "repo: $REPO"
echo
local kind src dst cond
while read -r kind src dst cond || [[ -n ${kind:-} ]]; do
[[ -z ${kind:-} || $kind == \#* ]] && continue
# A `hook` line has no target column: its destination is derived, so the condition shifts left.
if [[ $kind == hook ]]; then cond=$dst; dst="$HOOKS_DIR/$(basename "$src")"; fi
dst=${dst/#\~/$HOME}
cond=${cond:-}; cond=${cond/#\~/$HOME}
if [[ -n $cond && ! -e $cond ]]; then
say "skip" "$dst — no $cond on this machine"
continue
fi
link "$REPO/$src" "$dst" || true
done < "$MANIFEST"
echo
install_command
case ":$PATH:" in
*":$BIN_DIR:"*) ;;
*) echo; echo " note $BIN_DIR is not on PATH — add it, or mask-secret is not a command yet" ;;
esac
}
main "$@"