diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..58bf370 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,73 @@ +# Installing tools-public + +The installer registers the tools in this repo as user-level Claude Code slash commands by writing them into `~/.claude/commands/`. After install, each tool is invoked as `/` from any Claude Code session. + +## Install + +```bash +curl -fsSL https://raw.githubusercontent.com/cppalliance/tools-public/master/install.sh | bash +``` + +Drops 37 commands into `~/.claude/commands/`. Re-run anytime to update — existing files are overwritten with the latest version. **Restart Claude Code** afterwards to pick up new commands (Claude Code does not auto-reload `commands/`). + +## Uninstall + +```bash +curl -fsSL https://raw.githubusercontent.com/cppalliance/tools-public/master/uninstall.sh | bash +``` + +Removes only the files this installer placed. Anything else under `~/.claude/commands/` is left alone, and the directory itself is preserved. + +## Sample output + +```text +tools-public — Claude Code slash commands +========================================== + +A curated set of prompt-based "tools" from github.com/cppalliance/tools-public. +Each command is a markdown prompt invoked via / inside Claude Code, +covering code review, document tightening, plan refinement, persona voices, +adaptive interviews, tutorials, and more. + +Mode: install +Target: /Users/martin/.claude/commands +Source: downloading cppalliance/tools-public@master... + +Will install 37 commands to /Users/martin/.claude/commands + + + /advocatus Advocatus Diaboli, examiner, appointed adversary of the cause + + /auditor Mechanical compliance checker for WG21 papers + + /code-review Language-agnostic code review with adversarial challenge + + /lib-review Design analysis of open source projects using 38 diagnostic tests + + /refine-plan Refine any plan through numbering, audit, and compression + + /tighten Compress a document to half its length while preserving voice + + /voice A tool that builds tools. + + /voice:voice-of-william-gibson On load: you are William Gibson. + + /voice:voice-of-franz-kafka On load: you are Franz Kafka. + + /interview Generate role-specific adaptive interview protocols. + + /tutor Curriculum architect, scout of the world's best teaching + ... (26 more) + +Legend: + new ↻ overwrite (update) + +Proceed with install? [y/N] y +Installed 37 commands to /Users/martin/.claude/commands. +Restart Claude Code to pick them up. +``` + +On re-run, the `+` markers become `↻` for files already present. + +## Options + +Pass these as env vars before the curl pipe (or as flags to a local `bash install.sh`): + +| Env var | Effect | +| --- | --- | +| `INSTALL_YES=1` | Skip the `[y/N]` confirmation | +| `DEST=/path` | Install elsewhere than `~/.claude/commands` | +| `LOCAL_SRC=/path` | Use a local checkout instead of downloading the tarball | +| `UNINSTALL=1` | Run install.sh in uninstall mode (same as `uninstall.sh`) | + +## What's not included + +The novelist toolchain (`tools/novelist/`) is intentionally excluded — it's a coupled multi-prompt + Python pipeline that expects a per-book workspace and doesn't fit a one-shot command install. diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..04893fe --- /dev/null +++ b/install.sh @@ -0,0 +1,299 @@ +#!/usr/bin/env bash +# tools-public installer — Claude Code slash commands from +# github.com/cppalliance/tools-public. +# +# Install: +# curl -fsSL https://raw.githubusercontent.com/cppalliance/tools-public/master/install.sh | bash +# +# Uninstall (removes the same set, leaves your own commands alone): +# curl -fsSL https://raw.githubusercontent.com/cppalliance/tools-public/master/uninstall.sh | bash +# # or: +# UNINSTALL=1 curl -fsSL https://raw.githubusercontent.com/cppalliance/tools-public/master/install.sh | bash +# +# Re-run install to update to the latest versions; existing files are overwritten. +# Env knobs: +# INSTALL_YES=1 skip the [y/N] confirmation +# UNINSTALL=1 remove instead of install +# DEST=/path override ~/.claude/commands +# LOCAL_SRC=/path use a local checkout instead of downloading the tarball + +set -euo pipefail + +REPO="cppalliance/tools-public" +BRANCH="master" +TARBALL_URL="https://github.com/${REPO}/archive/refs/heads/${BRANCH}.tar.gz" +DEST="${DEST:-${HOME}/.claude/commands}" +LOCAL_SRC="${LOCAL_SRC:-}" + +# Mode: parse --uninstall flag or UNINSTALL env var. +MODE="install" +for arg in "$@"; do + case "$arg" in + --uninstall) MODE="uninstall" ;; + --help|-h) + sed -n '2,20p' "$0" + exit 0 + ;; + esac +done +[[ "${UNINSTALL:-}" == "1" ]] && MODE="uninstall" + +TOP_LEVEL=( + advocatus.md + auditor.md + boost-review.md + btc-talk.md + code-cleanup.md + code-review.md + herald.md + is-this-cpp.md + lib-review.md + normalize.md + refine-plan.md + research.md + review-paper.md + tighten.md +) + +FAMILIES=(voice interview tutor) + +die() { echo "error: $*" >&2; exit 1; } + +extract_description() { + local file="$1" + local desc="" + + desc="$(awk ' + BEGIN { in_fm = 0; line = 0 } + { line++ } + line == 1 && /^---[[:space:]]*$/ { in_fm = 1; next } + in_fm && /^---[[:space:]]*$/ { exit } + in_fm && /^description:/ { + sub(/^description:[[:space:]]*/, "") + gsub(/^"|"$/, "") + print + exit + } + ' "$file")" + + if [[ -z "$desc" ]]; then + desc="$(awk ' + BEGIN { seen = 0; in_comment = 0 } + /^#[^!]/ { seen = 1; next } + // { in_comment = 1; next } + in_comment && /-->/ { in_comment = 0; next } + in_comment { next } + /^[[:space:]]*$/ { next } + seen && NF > 0 && !/^---/ && !/^```/ && !/^!\[/ && !/^\|/ { print; exit } + ' "$file")" + fi + + desc="${desc//\`/}" + desc="${desc//\*\*/}" + + desc="$(printf '%s' "$desc" | awk '{ + n = index($0, ". ") + if (n > 0 && n < 90) print substr($0, 1, n) + else print substr($0, 1, 90) + }')" + + printf '%s' "$desc" +} + +# NAMES[i] = slash-command name (with leading /) +# SOURCES[i] = path to source .md inside the extracted tree +# TARGETS[i] = absolute path under $DEST +plan() { + local src="$1" + NAMES=() + SOURCES=() + TARGETS=() + + for f in "${TOP_LEVEL[@]}"; do + [[ -f "$src/$f" ]] || continue + NAMES+=("/${f%.md}") + SOURCES+=("$src/$f") + TARGETS+=("$DEST/$f") + done + + for family in "${FAMILIES[@]}"; do + if [[ -f "$src/$family/$family.md" ]]; then + NAMES+=("/$family") + SOURCES+=("$src/$family/$family.md") + TARGETS+=("$DEST/$family.md") + fi + if [[ -d "$src/$family" ]]; then + for f in "$src/$family"/*.md; do + [[ -e "$f" ]] || continue + local base + base="$(basename "$f" .md)" + [[ "$base" == "$family" ]] && continue + NAMES+=("/$family:$base") + SOURCES+=("$f") + TARGETS+=("$DEST/$family/$base.md") + done + fi + done +} + +print_banner() { + cat < inside Claude Code, +covering code review, document tightening, plan refinement, persona voices, +adaptive interviews, tutorials, and more. + +EOF +} + +print_plan() { + local action_word="$1" # "install" or "remove" + local present_count=0 + local i + for i in "${!TARGETS[@]}"; do + [[ -f "${TARGETS[$i]}" ]] && present_count=$((present_count + 1)) + done + + local max_width=0 + for name in "${NAMES[@]}"; do + (( ${#name} > max_width )) && max_width=${#name} + done + + if [[ "$action_word" == "install" ]]; then + echo "Will install ${#NAMES[@]} commands to $DEST" + if (( present_count > 0 )); then + echo "($present_count already present — those will be overwritten with the latest version.)" + fi + else + echo "Will remove ${present_count} commands from $DEST" + if (( present_count < ${#NAMES[@]} )); then + echo "($((${#NAMES[@]} - present_count)) listed below are not currently installed and will be skipped.)" + fi + fi + echo + + for i in "${!NAMES[@]}"; do + local desc marker=" " + desc="$(extract_description "${SOURCES[$i]}")" + if [[ "$action_word" == "install" ]]; then + [[ -f "${TARGETS[$i]}" ]] && marker="↻" || marker="+" + else + [[ -f "${TARGETS[$i]}" ]] && marker="-" || marker=" " + fi + printf " %s %-${max_width}s %s\n" "$marker" "${NAMES[$i]}" "$desc" + done + echo + + if [[ "$action_word" == "install" ]]; then + echo "Legend: + new ↻ overwrite (update)" + else + echo "Legend: - will be removed (blank) not installed, skipped" + fi + echo +} + +confirm() { + local prompt="$1" + if [[ "${INSTALL_YES:-}" == "1" ]]; then + return 0 + fi + if [[ ! -e /dev/tty ]]; then + die "no tty for confirmation; re-run with INSTALL_YES=1 to skip the prompt" + fi + local answer + read -r -p "$prompt [y/N] " answer /dev/null || true + fi + done + + echo "Removed $removed commands from $DEST." + (( skipped > 0 )) && echo "Skipped $skipped (not currently installed)." +} + +acquire_source() { + if [[ -n "$LOCAL_SRC" ]]; then + SRC="$LOCAL_SRC/tools" + [[ -d "$SRC" ]] || die "LOCAL_SRC=$LOCAL_SRC has no tools/ subdirectory" + echo "Source: local checkout at $LOCAL_SRC" + return + fi + + command -v curl >/dev/null || die "curl is required" + command -v tar >/dev/null || die "tar is required" + + TMP="$(mktemp -d)" + trap 'rm -rf "$TMP"' EXIT + + echo "Source: downloading ${REPO}@${BRANCH}..." + curl -fsSL "$TARBALL_URL" | tar -xz -C "$TMP" + + SRC="$(find "$TMP" -maxdepth 2 -type d -name tools | head -n 1)" + [[ -n "$SRC" && -d "$SRC" ]] || die "could not locate tools/ in extracted tarball" +} + +main() { + print_banner + echo "Mode: $MODE" + echo "Target: $DEST" + + acquire_source + + plan "$SRC" + [[ ${#NAMES[@]} -gt 0 ]] || die "nothing to process" + + echo + if [[ "$MODE" == "install" ]]; then + print_plan "install" + if confirm "Proceed with install?"; then + do_install + else + echo "Aborted. Nothing installed." + exit 1 + fi + else + print_plan "remove" + if confirm "Proceed with uninstall?"; then + do_uninstall + else + echo "Aborted. Nothing removed." + exit 1 + fi + fi +} + +main "$@" diff --git a/uninstall.sh b/uninstall.sh new file mode 100644 index 0000000..42e3860 --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Thin wrapper: download install.sh and run it in uninstall mode. +# +# Usage: +# curl -fsSL https://raw.githubusercontent.com/cppalliance/tools-public/master/uninstall.sh | bash + +set -euo pipefail + +REPO="cppalliance/tools-public" +BRANCH="master" +INSTALL_URL="https://raw.githubusercontent.com/${REPO}/${BRANCH}/install.sh" + +command -v curl >/dev/null || { echo "error: curl is required" >&2; exit 1; } + +curl -fsSL "$INSTALL_URL" | UNINSTALL=1 bash