From 71fe19da67411ea0410d523688eb84a27191f6c7 Mon Sep 17 00:00:00 2001 From: mini Date: Thu, 18 Jun 2026 10:11:39 +0900 Subject: [PATCH] feat: add claw-code-style build system (source build, CI, Containerfile) - install.sh: full rewrite with step-by-step colored UI, source build, prereq checks, --update flag, auto-clone, troubleshooting guide - scripts/dogfood-build.sh: provenance-checked build with GIT_SHA injection - scripts/validate-agents.sh: lint agent MD files - scripts/fmt.sh: unified Rust + Python formatter - .github/workflows/ci.yml: PR gate with fmt, clippy, test, Windows smoke - Containerfile: Docker/Podman image for reproducible source builds - Makefile: dogfood, container, validate, fmt-check targets Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yml | 157 ++++++++++++ Containerfile | 26 ++ Makefile | 25 +- install.sh | 494 ++++++++++++++++++++++++++----------- scripts/dogfood-build.sh | 67 +++++ scripts/fmt.sh | 55 +++++ scripts/validate-agents.sh | 77 ++++++ 7 files changed, 744 insertions(+), 157 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 Containerfile create mode 100644 scripts/dogfood-build.sh create mode 100644 scripts/fmt.sh create mode 100644 scripts/validate-agents.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f983769 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,157 @@ +name: CI + +on: + push: + branches: + - main + - 'feat/**' + - 'fix/**' + - 'chore/**' + paths: + - '.github/workflows/ci.yml' + - 'rust/**' + - 'agents/**' + - 'tools/**' + - 'scripts/**' + pull_request: + branches: + - main + paths: + - '.github/workflows/ci.yml' + - 'rust/**' + - 'agents/**' + - 'tools/**' + - 'scripts/**' + workflow_dispatch: + +concurrency: + group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +defaults: + run: + working-directory: rust + +# ── Job: validate agents ───────────────────────────────────────────────────── +jobs: + validate-agents: + name: Validate agent files + runs-on: ubuntu-latest + defaults: + run: + working-directory: . + steps: + - uses: actions/checkout@v4 + + - name: Check agent MD files + run: bash scripts/validate-agents.sh --strict + + # ── Job: rustfmt ───────────────────────────────────────────────────────────── + fmt: + name: cargo fmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: rust -> target + + - name: Check formatting + run: cargo fmt --all --check + + # ── Job: clippy ────────────────────────────────────────────────────────────── + clippy: + name: cargo clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: rust -> target + + - name: Run clippy + run: cargo clippy --workspace -- -D warnings + + # ── Job: test ──────────────────────────────────────────────────────────────── + test: + name: cargo test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: rust -> target + + - name: Run workspace tests + run: cargo test --workspace + + # ── Job: Windows smoke build ───────────────────────────────────────────────── + windows-smoke: + name: Windows build smoke + runs-on: windows-latest + defaults: + run: + working-directory: rust + shell: pwsh + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: rust -> target + + - name: Build (debug) + run: cargo build --workspace + + - name: Smoke test binary + run: | + $bin = "target\debug\claude-tools.exe" + if (Test-Path $bin) { + & $bin --version + Write-Host "claude-tools binary OK" + } else { + Write-Host "No claude-tools binary found (workspace may not include binary crate yet)" + } + + # ── Job: dogfood build ─────────────────────────────────────────────────────── + dogfood: + name: Dogfood build (Linux) + runs-on: ubuntu-latest + needs: [fmt, clippy, test] + defaults: + run: + working-directory: . + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: rust -> target + + - name: Build via dogfood script + run: | + bash scripts/dogfood-build.sh + + - name: Validate agents with built binary + run: bash scripts/validate-agents.sh diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..53b8cf3 --- /dev/null +++ b/Containerfile @@ -0,0 +1,26 @@ +FROM rust:bookworm + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + git \ + libssl-dev \ + pkg-config \ + python3 \ + python3-pip \ + && rm -rf /var/lib/apt/lists/* + +ENV CARGO_TERM_COLOR=always +WORKDIR /workspace + +# Pre-fetch dependencies so layer is cached +COPY rust/Cargo.toml rust/Cargo.lock ./rust/ +RUN cd rust && cargo fetch --locked 2>/dev/null || true + +# Copy full source +COPY . . + +RUN cd rust && cargo build --workspace --release \ + && cp target/release/claude-tools /usr/local/bin/claude-tools + +CMD ["bash"] diff --git a/Makefile b/Makefile index e5ac109..1189301 100644 --- a/Makefile +++ b/Makefile @@ -106,21 +106,32 @@ test-agents: ## Verify agent files exist and are non-empty echo "Agents: $$ok OK, $$fail empty" # ─── lint / format ───────────────────────────────────────────────────────── -.PHONY: lint fmt +.PHONY: lint fmt fmt-check lint: ## Clippy lint (Rust) cd $(RUST_DIR) && $(CARGO) clippy -- -D warnings -fmt: ## rustfmt + check Python style - cd $(RUST_DIR) && $(CARGO) fmt - @command -v ruff >/dev/null 2>&1 \ - && ruff format $(TOOLS_DIR)/ \ - || echo "ruff not found, skipping Python format" +fmt: ## Format all code (Rust + Python) + bash scripts/fmt.sh + +fmt-check: ## Check formatting without modifying (CI mode) + bash scripts/fmt.sh --check # ─── dev helpers ─────────────────────────────────────────────────────────── -.PHONY: watch-cost env status +.PHONY: watch-cost env status dogfood container validate watch-cost: ## Live cost monitor (requires claude-tools build) @$(RUST_DIR)/target/release/claude-tools watch --interval 2 +dogfood: ## Build from source with provenance check (like claw-code) + @bash scripts/dogfood-build.sh + +container: ## Build Containerfile (requires Docker / Podman) + @command -v docker >/dev/null 2>&1 && docker build -f Containerfile -t claude-agents:dev . \ + || command -v podman >/dev/null 2>&1 && podman build -f Containerfile -t claude-agents:dev . \ + || { echo "docker or podman required"; exit 1; } + +validate: ## Validate agent MD files + @bash scripts/validate-agents.sh + env: ## Show Claude environment status @$(RUST_DIR)/target/release/claude-tools env diff --git a/install.sh b/install.sh index 7d98d61..13d3365 100644 --- a/install.sh +++ b/install.sh @@ -1,194 +1,388 @@ #!/usr/bin/env bash # Claude Code Multi-Agent System — Installer +# +# Clones (or updates) the repo, verifies prerequisites, builds the Rust +# workspace from source, installs agents + slash commands + Python tools, +# and verifies the result. +# # Usage: +# # From the web (recommended) # curl -fsSL https://raw.githubusercontent.com/BcKmini/claude-code-use/main/install.sh | bash -# curl -fsSL ... | bash -s -- --version v1.2.0 # specific version -# curl -fsSL ... | bash -s -- --no-binary # agents + tools only, skip Rust binary +# +# # From a cloned repo +# ./install.sh +# ./install.sh --release # optimized build (slower compile, faster binary) +# ./install.sh --no-binary # skip Rust build; agents + Python tools only +# ./install.sh --no-verify # skip post-install verification +# ./install.sh --update # pull latest and reinstall +# ./install.sh --help +# +# Environment overrides: +# CLAUDE_HOME default: ~/.claude +# BIN_DIR default: ~/.local/bin +# BUILD_PROFILE debug | release +# SKIP_VERIFY 1 to skip +# SKIP_BINARY 1 to skip Rust build + set -euo pipefail -REPO="BcKmini/claude-code-use" +# ── colour support ────────────────────────────────────────────────────────── +if [ -t 1 ] && command -v tput >/dev/null 2>&1 && [ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]; then + R="$(tput sgr0)"; BOLD="$(tput bold)"; DIM="$(tput dim)" + RED="$(tput setaf 1)"; GREEN="$(tput setaf 2)"; YELLOW="$(tput setaf 3)" + BLUE="$(tput setaf 4)"; CYAN="$(tput setaf 6)"; PURPLE="$(tput setaf 5)" +else + R=""; BOLD=""; DIM=""; RED=""; GREEN=""; YELLOW=""; BLUE=""; CYAN=""; PURPLE="" +fi + +STEP_N=0 +STEP_TOTAL=7 + +step() { + STEP_N=$((STEP_N + 1)) + printf '\n%s[%d/%d]%s %s%s%s\n' \ + "${BLUE}" "${STEP_N}" "${STEP_TOTAL}" "${R}" "${BOLD}" "$1" "${R}" +} +info() { printf '%s →%s %s\n' "${CYAN}" "${R}" "$*"; } +ok() { printf '%s ✓%s %s\n' "${GREEN}" "${R}" "$*"; } +warn() { printf '%s ⚠%s %s\n' "${YELLOW}" "${R}" "$*" >&2; } +error() { printf '%s ✗%s %s\n' "${RED}" "${R}" "$*" >&2; } +section() { printf '\n%s%s%s\n%s%s%s\n' "${BOLD}${PURPLE}" "$*" "${R}" "${DIM}" "$(printf '%.0s─' $(seq 1 ${#1}))" "${R}"; } + +REPO_URL="https://github.com/BcKmini/claude-code-use.git" CLAUDE_HOME="${CLAUDE_HOME:-$HOME/.claude}" BIN_DIR="${BIN_DIR:-$HOME/.local/bin}" -VERSION="" -INSTALL_BINARY=true +BUILD_PROFILE="${BUILD_PROFILE:-release}" +SKIP_VERIFY="${SKIP_VERIFY:-0}" +SKIP_BINARY="${SKIP_BINARY:-0}" +DO_UPDATE=0 + +# ── banner ────────────────────────────────────────────────────────────────── +print_banner() { + printf '%s' "${BOLD}${PURPLE}" + cat <<'EOF' + + ██████╗██╗ █████╗ ██╗ ██╗██████╗ ███████╗ + ██╔════╝██║ ██╔══██╗██║ ██║██╔══██╗██╔════╝ + ██║ ██║ ███████║██║ ██║██║ ██║█████╗ + ██║ ██║ ██╔══██║██║ ██║██║ ██║██╔══╝ + ╚██████╗███████╗██║ ██║╚██████╔╝██████╔╝███████╗ + ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝ + + Claude Code Multi-Agent System · Installer +EOF + printf '%s\n\n' "${R}" +} + +# ── help ──────────────────────────────────────────────────────────────────── +print_help() { + cat <> ~/.bashrc + source ~/.bashrc -# ── parse args ─────────────────────────────────────────────────────────────── -while [[ $# -gt 0 ]]; do + ${BOLD}7. Agents not showing in /agents${R} + Restart Claude Code after install. + Verify: ls ~/.claude/agents/ + +EOF +} + +# ── arg parse ──────────────────────────────────────────────────────────────── +while [ "$#" -gt 0 ]; do case "$1" in - --version) VERSION="$2"; shift 2 ;; - --no-binary) INSTALL_BINARY=false; shift ;; - *) echo "Unknown option: $1"; exit 1 ;; + --release) BUILD_PROFILE="release" ;; + --debug) BUILD_PROFILE="debug" ;; + --no-binary) SKIP_BINARY="1" ;; + --no-verify) SKIP_VERIFY="1" ;; + --update) DO_UPDATE=1 ;; + -h|--help) print_help; exit 0 ;; + *) error "Unknown argument: $1"; print_help; exit 2 ;; esac + shift done +trap 'rc=$?; [ "$rc" -ne 0 ] && { error "Installation failed (exit $rc)."; print_troubleshooting; }' EXIT + # ── helpers ────────────────────────────────────────────────────────────────── -info() { printf '\033[1;36m[claude-install]\033[0m %s\n' "$*"; } -success() { printf '\033[1;32m[claude-install]\033[0m %s\n' "$*"; } -warn() { printf '\033[1;33m[claude-install]\033[0m %s\n' "$*" >&2; } -error() { printf '\033[1;31m[claude-install]\033[0m %s\n' "$*" >&2; exit 1; } - -need_cmd() { command -v "$1" &>/dev/null || error "Required command not found: $1"; } - -# ── detect OS / arch ───────────────────────────────────────────────────────── -detect_target() { - local os arch - os="$(uname -s)" - arch="$(uname -m)" - - case "$os" in - Linux) - case "$arch" in - x86_64) echo "x86_64-unknown-linux-musl" ;; - *) error "Unsupported Linux arch: $arch. Build from source with: cargo build --release" ;; - esac - ;; - Darwin) - case "$arch" in - x86_64) echo "x86_64-apple-darwin" ;; - arm64) echo "aarch64-apple-darwin" ;; - *) error "Unsupported macOS arch: $arch" ;; - esac - ;; - *) error "Unsupported OS: $os. Use install.ps1 on Windows." ;; - esac -} +need_cmd() { command -v "$1" &>/dev/null; } +require() { need_cmd "$1" || { error "Required command not found: $1"; exit 1; }; } -# ── resolve version ─────────────────────────────────────────────────────────── -resolve_version() { - if [ -n "$VERSION" ]; then - # strip leading 'v' if user passed it - echo "${VERSION#v}" +# ── main ───────────────────────────────────────────────────────────────────── +print_banner + +# ── Step 1: Detect environment ─────────────────────────────────────────────── +step "Detecting host environment" + +UNAME_S="$(uname -s 2>/dev/null || echo unknown)" +UNAME_M="$(uname -m 2>/dev/null || echo unknown)" +OS="unknown" +IS_WSL=0 + +case "${UNAME_S}" in + Linux*) + OS="linux" + grep -qiE 'microsoft|wsl' /proc/version 2>/dev/null && IS_WSL=1 + ;; + Darwin*) OS="macos" ;; + MINGW*|MSYS*|CYGWIN*) + error "Detected native Windows shell. Please run this inside WSL." + exit 1 + ;; +esac + +info "OS: ${UNAME_S} ${UNAME_M}" +info "Home: ${CLAUDE_HOME}" +info "Bin: ${BIN_DIR}" +[ "$IS_WSL" -eq 1 ] && info "WSL: yes" +ok "Platform supported" + +# ── Step 2: Check prerequisites ────────────────────────────────────────────── +step "Checking prerequisites" + +MISSING=0 + +need_cmd git && ok "git $(git --version 2>/dev/null)" || { warn "git not found (some features may degrade)"; } +need_cmd python3 && ok "python3 $(python3 --version 2>/dev/null)" || warn "python3 not found — Python tools won't install" + +if [ "$SKIP_BINARY" = "0" ]; then + if need_cmd rustc; then + ok "rustc $(rustc --version 2>/dev/null)" else - need_cmd curl - curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \ - | grep '"tag_name"' \ - | sed -E 's/.*"v([^"]+)".*/\1/' + warn "rustc not found — skipping Rust build (use --no-binary to suppress this)" + warn "Install Rust: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh" + SKIP_BINARY=1 fi -} -# ── install agents + slash commands ────────────────────────────────────────── -install_agents() { - local version="$1" - info "Installing agents v${version}..." + if need_cmd cargo; then + ok "cargo $(cargo --version 2>/dev/null)" + else + SKIP_BINARY=1 + fi - need_cmd curl unzip + if [ "$OS" = "linux" ]; then + need_cmd pkg-config && ok "pkg-config found" || warn "pkg-config missing — may be needed for OpenSSL" + fi - local tmp - tmp="$(mktemp -d)" - trap 'rm -rf "$tmp"' EXIT + if [ "$OS" = "macos" ] && ! need_cmd cc; then + warn "Xcode CLT not detected — run: xcode-select --install" + fi +fi - curl -fsSL \ - "https://github.com/${REPO}/releases/download/v${version}/claude-agents-${version}.zip" \ - -o "$tmp/agents.zip" +[ "$MISSING" -eq 0 ] && ok "Prerequisites satisfied" - unzip -q "$tmp/agents.zip" -d "$tmp/extracted" +# ── Step 3: Locate / clone repo ────────────────────────────────────────────── +step "Locating repository" - mkdir -p "${CLAUDE_HOME}/agents" "${CLAUDE_HOME}/commands" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd || pwd)" +REPO_DIR="$SCRIPT_DIR" - # copy agents - local count=0 - for f in "$tmp/extracted/agents/"*.md; do - cp "$f" "${CLAUDE_HOME}/agents/" - count=$((count + 1)) - done - success " ✓ $count agents → ${CLAUDE_HOME}/agents/" - - # copy slash commands - count=0 - for f in "$tmp/extracted/.claude/commands/"*.md; do - [ -f "$f" ] || continue - cp "$f" "${CLAUDE_HOME}/commands/" - count=$((count + 1)) - done - success " ✓ $count slash commands → ${CLAUDE_HOME}/commands/" -} +# If run via curl (no local repo), clone it +if [ ! -f "${REPO_DIR}/agents/00-orchestrator.md" ]; then + info "No local repo detected — cloning ${REPO_URL}" + need_cmd git || { error "git is required to clone the repository"; exit 1; } + CLONE_TARGET="$HOME/.claude-code-agents-src" + if [ -d "$CLONE_TARGET/.git" ]; then + info "Existing clone found at ${CLONE_TARGET} — pulling latest" + git -C "$CLONE_TARGET" pull --ff-only + else + git clone --depth=1 "$REPO_URL" "$CLONE_TARGET" + fi + REPO_DIR="$CLONE_TARGET" + ok "Repository at ${REPO_DIR}" +elif [ "$DO_UPDATE" -eq 1 ]; then + info "Pulling latest changes..." + git -C "$REPO_DIR" pull --ff-only + ok "Repository up-to-date" +else + ok "Using local repo at ${REPO_DIR}" +fi + +RUST_DIR="${REPO_DIR}/rust" + +# ── Step 4: Build Rust workspace ───────────────────────────────────────────── +if [ "$SKIP_BINARY" = "0" ]; then + step "Building Rust workspace (profile: ${BUILD_PROFILE})" + + [ -d "$RUST_DIR" ] || { error "rust/ directory not found in ${REPO_DIR}"; exit 1; } + [ -f "${RUST_DIR}/Cargo.toml" ] || { error "Cargo.toml not found in ${RUST_DIR}"; exit 1; } + + CARGO_FLAGS=(build --workspace) + [ "$BUILD_PROFILE" = "release" ] && CARGO_FLAGS+=(--release) + + info "Running: cargo ${CARGO_FLAGS[*]}" + info "First build may take a few minutes…" + echo "" + + ( + cd "${RUST_DIR}" + CARGO_TERM_COLOR=always cargo "${CARGO_FLAGS[@]}" + ) -# ── install Python tools ───────────────────────────────────────────────────── -install_python_tools() { - local version="$1" - info "Installing Python tools v${version}..." + CLAUDE_TOOLS_BIN="${RUST_DIR}/target/${BUILD_PROFILE}/claude-tools" + [ -x "$CLAUDE_TOOLS_BIN" ] || { error "Binary not found at ${CLAUDE_TOOLS_BIN}"; exit 1; } + ok "Built → ${CLAUDE_TOOLS_BIN}" - local tmp - tmp="$(mktemp -d)" - trap 'rm -rf "$tmp"' EXIT + mkdir -p "$BIN_DIR" + cp "$CLAUDE_TOOLS_BIN" "${BIN_DIR}/claude-tools" + chmod +x "${BIN_DIR}/claude-tools" + ok "Installed claude-tools → ${BIN_DIR}/claude-tools" +else + step "Skipping Rust build (--no-binary)" + info "claude-tools binary will not be installed" + STEP_TOTAL=$((STEP_TOTAL - 1)) +fi + +# ── Step 5: Install agents ──────────────────────────────────────────────────── +step "Installing agents → ${CLAUDE_HOME}/agents/" + +mkdir -p "${CLAUDE_HOME}/agents" +count=0 +for f in "${REPO_DIR}/agents/"*.md; do + [ -f "$f" ] || continue + cp "$f" "${CLAUDE_HOME}/agents/" + count=$((count + 1)) +done +ok "${count} agents installed" - curl -fsSL \ - "https://github.com/${REPO}/releases/download/v${version}/claude-tools-python-${version}.zip" \ - -o "$tmp/pytools.zip" +# ── Step 6: Install slash commands + Python tools ───────────────────────────── +step "Installing slash commands + Python tools" - unzip -q "$tmp/pytools.zip" -d "$tmp/extracted" +mkdir -p "${CLAUDE_HOME}/commands" +count=0 +for f in "${REPO_DIR}/.claude/commands/"*.md; do + [ -f "$f" ] || continue + cp "$f" "${CLAUDE_HOME}/commands/" + count=$((count + 1)) +done +ok "${count} slash commands → ${CLAUDE_HOME}/commands/" +if need_cmd python3; then mkdir -p "$BIN_DIR" - - local tools=(snippet claude-handoff claude-cost claude-review-diff claude-remind claude-harness claude-pipeline) - for tool in "${tools[@]}"; do - local src="$tmp/extracted/tools/${tool}.py" - if [ -f "$src" ]; then - cp "$src" "${BIN_DIR}/${tool}" - chmod +x "${BIN_DIR}/${tool}" - success " ✓ $tool → ${BIN_DIR}/${tool}" - fi + TOOLS=(snippet claude-handoff claude-cost claude-review-diff claude-remind claude-harness claude-pipeline) + for tool in "${TOOLS[@]}"; do + src="${REPO_DIR}/tools/${tool}.py" + [ -f "$src" ] || continue + cp "$src" "${BIN_DIR}/${tool}" + chmod +x "${BIN_DIR}/${tool}" + ok "${tool} → ${BIN_DIR}/${tool}" done -} +else + warn "python3 not found — skipping Python tool installation" +fi + +# ── Step 7: Verify ──────────────────────────────────────────────────────────── +step "Verifying installation" + +FAIL=0 +INSTALLED_AGENTS=$(ls "${CLAUDE_HOME}/agents/"*.md 2>/dev/null | wc -l | tr -d ' ') + +if [ "$INSTALLED_AGENTS" -gt 0 ]; then + ok "${INSTALLED_AGENTS} agents confirmed in ${CLAUDE_HOME}/agents/" +else + error "No agents found in ${CLAUDE_HOME}/agents/ — something went wrong" + FAIL=1 +fi + +if [ "$SKIP_VERIFY" = "0" ] && [ "$SKIP_BINARY" = "0" ] && [ -x "${BIN_DIR}/claude-tools" ]; then + info "Running: claude-tools --version" + if VER="$("${BIN_DIR}/claude-tools" --version 2>&1)"; then + ok "claude-tools → ${VER}" + else + error "claude-tools --version failed" + FAIL=1 + fi +fi -# ── install Rust binary ─────────────────────────────────────────────────────── -install_binary() { - local version="$1" target="$2" - info "Installing claude-tools binary (${target}) v${version}..." +[ "$FAIL" -eq 0 ] || exit 1 - local tmp - tmp="$(mktemp -d)" - trap 'rm -rf "$tmp"' EXIT +# ── Done ───────────────────────────────────────────────────────────────────── +VERSION="$(cat "${REPO_DIR}/VERSION" 2>/dev/null || echo '?')" - local archive="claude-tools-${version}-${target}.tar.gz" - curl -fsSL \ - "https://github.com/${REPO}/releases/download/v${version}/${archive}" \ - -o "$tmp/${archive}" +cat <&2; usage >&2; exit 2 ;; + esac +fi + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +RUST_DIR="$REPO_ROOT/rust" +PROFILE="${BUILD_PROFILE:-debug}" +BINARY="$RUST_DIR/target/${PROFILE}/claude-tools" +EXPECTED_SHA="$(git -C "$REPO_ROOT" rev-parse --short HEAD)" + +echo " Building claude-tools from ${REPO_ROOT}" >&2 +echo " Commit: $(git -C "$REPO_ROOT" log --oneline -1)" >&2 +echo " Profile: ${PROFILE}" >&2 + +CARGO_FLAGS=(build --workspace) +[ "$PROFILE" = "release" ] && CARGO_FLAGS+=(--release) + +if ! GIT_SHA="$EXPECTED_SHA" CARGO_TERM_COLOR=always \ + cargo "${CARGO_FLAGS[@]}" \ + --manifest-path "$RUST_DIR/Cargo.toml" -q 2>/dev/null; then + echo " Build failed — rerunning with output:" >&2 + GIT_SHA="$EXPECTED_SHA" cargo "${CARGO_FLAGS[@]}" \ + --manifest-path "$RUST_DIR/Cargo.toml" 2>&1 | sed 's/^/ /' >&2 + exit 1 +fi + +[[ -x "$BINARY" ]] || { echo " Binary not found: $BINARY" >&2; exit 1; } + +BINARY_SHA=$("$BINARY" version 2>/dev/null | grep -o 'sha:[a-f0-9]*' | cut -d: -f2 || echo "null") + +if [[ "$BINARY_SHA" == "null" || -z "$BINARY_SHA" ]]; then + echo " ⚠ Provenance check skipped (binary has no SHA output)" >&2 +else + if [[ "$BINARY_SHA" != "$EXPECTED_SHA" ]]; then + echo " Provenance mismatch: binary=${BINARY_SHA}, HEAD=${EXPECTED_SHA}" >&2 + exit 1 + fi + echo " Binary verified: ${BINARY_SHA} == HEAD" >&2 +fi + +echo "" >&2 +echo " Binary: ${BINARY}" >&2 +echo " Run time overhead vs pre-built: ~1s for cargo run vs ~5ms for binary." >&2 +echo "" >&2 +echo "$BINARY" diff --git a/scripts/fmt.sh b/scripts/fmt.sh new file mode 100644 index 0000000..75fab41 --- /dev/null +++ b/scripts/fmt.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# fmt.sh — Format all code in the repo. +# +# Usage: +# bash scripts/fmt.sh # format in place +# bash scripts/fmt.sh --check # exit 1 if any file needs reformatting (CI mode) + +set -euo pipefail + +CHECK=0 +[ "${1:-}" = "--check" ] && CHECK=1 + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +RUST_DIR="$REPO_ROOT/rust" + +ok() { printf '\033[32m ✓\033[0m %s\n' "$*"; } +fail() { printf '\033[31m ✗\033[0m %s\n' "$*"; } + +FAILURES=0 + +# ── Rust ────────────────────────────────────────────────────────────────── +if command -v cargo &>/dev/null && [ -d "$RUST_DIR" ]; then + if [ "$CHECK" -eq 1 ]; then + if cargo fmt --manifest-path "$RUST_DIR/Cargo.toml" --all --check; then + ok "Rust: cargo fmt check passed" + else + fail "Rust: cargo fmt check failed — run: bash scripts/fmt.sh" + FAILURES=$((FAILURES+1)) + fi + else + cargo fmt --manifest-path "$RUST_DIR/Cargo.toml" --all + ok "Rust: cargo fmt applied" + fi +else + printf ' skipping Rust (cargo not found)\n' +fi + +# ── Python ───────────────────────────────────────────────────────────────── +if command -v ruff &>/dev/null; then + if [ "$CHECK" -eq 1 ]; then + if ruff format --check "$REPO_ROOT/tools/" 2>/dev/null; then + ok "Python: ruff format check passed" + else + fail "Python: ruff format check failed — run: bash scripts/fmt.sh" + FAILURES=$((FAILURES+1)) + fi + else + ruff format "$REPO_ROOT/tools/" 2>/dev/null && ok "Python: ruff format applied" + fi +else + printf ' skipping Python (ruff not found)\n' +fi + +echo "" +[ "$FAILURES" -eq 0 ] || exit 1 diff --git a/scripts/validate-agents.sh b/scripts/validate-agents.sh new file mode 100644 index 0000000..96c8e22 --- /dev/null +++ b/scripts/validate-agents.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# validate-agents.sh — Verify agent MD files are well-formed. +# +# Checks: +# 1. Every NN-name.md file is non-empty +# 2. Required sections exist (## Role, ## Instructions or ## Core Rules) +# 3. No duplicate agent numbers +# 4. Agent names match the expected numbering sequence +# +# Usage: +# bash scripts/validate-agents.sh +# bash scripts/validate-agents.sh --strict # exit 1 on warnings too + +set -euo pipefail + +AGENTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/agents" +STRICT=0 + +[ "$*" = "--strict" ] && STRICT=1 + +ok() { printf '\033[32m ✓\033[0m %s\n' "$*"; } +warn() { printf '\033[33m ⚠\033[0m %s\n' "$*"; [ "$STRICT" -eq 0 ] || exit 1; } +fail() { printf '\033[31m ✗\033[0m %s\n' "$*"; FAILURES=$((FAILURES+1)); } + +FAILURES=0 + +if [ ! -d "$AGENTS_DIR" ]; then + fail "agents/ directory not found at $AGENTS_DIR" + exit 1 +fi + +declare -A SEEN_NUMS + +for f in "$AGENTS_DIR"/[0-9][0-9]-*.md; do + [ -f "$f" ] || { warn "No agent files found in $AGENTS_DIR"; break; } + + base="$(basename "$f")" + num="${base:0:2}" + + # Duplicate number check + if [ "${SEEN_NUMS[$num]+x}" ]; then + fail "Duplicate agent number: $num (${SEEN_NUMS[$num]} and $base)" + fi + SEEN_NUMS[$num]="$base" + + # Non-empty + if [ ! -s "$f" ]; then + fail "Empty agent file: $base" + continue + fi + + # Required sections + if ! grep -q '^## ' "$f"; then + fail "$base: No H2 sections found" + continue + fi + + # Role or name header + if ! grep -qiE '^## (Role|Name|Identity|에이전트)' "$f"; then + warn "$base: No '## Role' or '## Name' section found" + fi + + # Instructions section + if ! grep -qiE '^## (Instructions|Core Rules|Rules|규칙|지침)' "$f"; then + warn "$base: No '## Instructions' / '## Core Rules' section found" + fi + + ok "$base" +done + +echo "" +if [ "$FAILURES" -gt 0 ]; then + printf '\033[31m %d validation failure(s)\033[0m\n\n' "$FAILURES" + exit 1 +else + printf '\033[32m All agents valid\033[0m\n\n' +fi