diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh new file mode 100755 index 0000000..fd9c989 --- /dev/null +++ b/.claude/hooks/session-start.sh @@ -0,0 +1,143 @@ +#!/bin/bash +set -euo pipefail + +# SessionStart hook: install a Swift toolchain and lint tooling for Claude +# Code on the web (Linux). Only runs in remote sessions; local sessions are +# untouched. Runs async so the session starts immediately: progress lands in +# ~/.claude-session-setup.log and ~/.claude-session-setup.done marks the end. +if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then + exit 0 +fi + +echo '{"async": true, "asyncTimeout": 2400000}' + +SETUP_LOG="$HOME/.claude-session-setup.log" +SETUP_DONE="$HOME/.claude-session-setup.done" +rm -f "$SETUP_DONE" +exec >> "$SETUP_LOG" 2>&1 + +SWIFTLY_ENV="$HOME/.local/share/swiftly/env.sh" +PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}" +TOOLS_BIN="$HOME/.local/bin" + +# Make swift and the lint tools reachable for the session up front; entries +# pointing at not-yet-populated directories are harmless. +if [ -n "${CLAUDE_ENV_FILE:-}" ]; then + { + echo "export SWIFTLY_HOME_DIR=\"$HOME/.local/share/swiftly\"" + echo "export SWIFTLY_BIN_DIR=\"$HOME/.local/share/swiftly/bin\"" + echo "export PATH=\"$HOME/.local/share/swiftly/bin:$TOOLS_BIN:\$PATH\"" + } >> "$CLAUDE_ENV_FILE" +fi + +install_swift() { + # System dependencies for Swift on Ubuntu 24.04 (per swift.org Linux + # instructions), plus curl for fetching swiftly. + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y -qq \ + binutils \ + curl \ + git \ + gnupg2 \ + libc6-dev \ + libcurl4-openssl-dev \ + libedit2 \ + libgcc-13-dev \ + libncurses-dev \ + libpython3-dev \ + libsqlite3-0 \ + libstdc++-13-dev \ + libxml2-dev \ + libz3-dev \ + pkg-config \ + tzdata \ + unzip \ + zlib1g-dev + + # Install swiftly non-interactively, then the toolchain pinned by the + # repo's .swift-version (falling back to latest if no pin resolves). + local workdir + workdir="$(mktemp -d)" + pushd "$workdir" > /dev/null + curl -fsSLO "https://download.swift.org/swiftly/linux/swiftly-$(uname -m).tar.gz" + tar zxf "swiftly-$(uname -m).tar.gz" + ./swiftly init -y --skip-install + popd > /dev/null + rm -rf "$workdir" + + # shellcheck disable=SC1090 + . "$SWIFTLY_ENV" + + cd "$PROJECT_DIR" + if ! swiftly install -y; then + echo "Pinned toolchain install failed; falling back to latest." >&2 + swiftly install -y latest + swiftly use -y latest + fi +} + +# Read a tool's pinned version out of mise.toml so the pins have one source +# of truth shared with CI and local dev. +mise_pin() { + sed -n "s|.*$1\" *= *\"\([^\"]*\)\".*|\1|p" "$PROJECT_DIR/mise.toml" +} + +# Install SwiftLint from its prebuilt Linux release binary. Web sessions +# cannot use `mise install` for this: the session's GitHub gateway scopes +# api.github.com to repos attached to the session, and mise's version +# resolution 403s on the tool repos. Anonymous release-asset downloads do +# work, so the hook installs the same pinned version through that path. +# The other lint tools are deliberately NOT installed here: swift-format +# ships inside the Swift toolchain (swiftly proxies it), and periphery is +# skipped in web sessions entirely (Scripts/lint.sh omits the scan when +# CLAUDE_CODE_REMOTE is set), keeping session cold-start fast. +install_lint_tools() { + local swiftlint_version workdir + swiftlint_version="$(mise_pin 'aqua:realm/SwiftLint')" + mkdir -p "$TOOLS_BIN" + export PATH="$TOOLS_BIN:$PATH" + + if command -v swiftlint > /dev/null 2>&1 \ + && [ "$(swiftlint --version)" = "$swiftlint_version" ]; then + echo "SwiftLint $swiftlint_version already installed." + else + workdir="$(mktemp -d)" + curl -fsSL -o "$workdir/swiftlint.zip" \ + "https://github.com/realm/SwiftLint/releases/download/$swiftlint_version/swiftlint_linux_amd64.zip" + unzip -q -o "$workdir/swiftlint.zip" -d "$workdir" + install -m 755 "$workdir/swiftlint" "$TOOLS_BIN/swiftlint" + rm -rf "$workdir" + echo "SwiftLint $swiftlint_version installed." + fi +} + +# Pick up a swiftly install from a previous (cached) hook run. +if [ -f "$SWIFTLY_ENV" ]; then + # shellcheck disable=SC1090 + . "$SWIFTLY_ENV" +fi + +if command -v swift > /dev/null 2>&1; then + echo "Swift already installed: $(swift --version 2>&1 | head -1)" +else + install_swift +fi + +# Lint tooling is secondary to the toolchain: warn loudly on failure but +# leave the session usable for building and testing. +if ! install_lint_tools; then + echo "WARNING: lint tooling install failed; make lint will not work." >&2 + echo "WARNING: swift build/test are unaffected. See errors above." >&2 +fi + +# SwiftLint on Linux dlopens libsourcekitdInProc.so and finds it through +# LINUX_SOURCEKIT_LIB_PATH; resolve it now that the toolchain exists. +sourcekit_lib="$(find "$HOME/.local/share/swiftly/toolchains" \ + -name libsourcekitdInProc.so -exec dirname {} \; 2> /dev/null | head -1)" +if [ -n "$sourcekit_lib" ] && [ -n "${CLAUDE_ENV_FILE:-}" ]; then + echo "export LINUX_SOURCEKIT_LIB_PATH=\"$sourcekit_lib\"" >> "$CLAUDE_ENV_FILE" +fi + +swift --version +touch "$SETUP_DONE" diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..e06b033 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh" + } + ] + } + ] + } +} diff --git a/CLAUDE.md b/CLAUDE.md index 4b071e9..25340ad 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,7 +14,7 @@ ConfigKeyKit is a tiny, **dependency-free, Foundation-only** Swift 6.2 library ( - `make lint` — runs `Scripts/lint.sh`: swift-format, SwiftLint, license-header check, and `periphery` dead-code scan - `make clean` -Lint/format tooling is pinned via **mise** (`mise.toml`): swift-format 602.0.0, SwiftLint 0.62.2, periphery 3.7.4. Run `mise install` once so `Scripts/lint.sh` can find them outside CI. `Scripts/lint.sh` is env-driven: `LINT_MODE` (`STRICT` adds `--strict`/`--configuration`; `NONE`/`INSTALL` short-circuit), `FORMAT_ONLY=1` skips lint+build, and outside CI it auto-formats in place before linting. +Lint/format tooling is pinned via **mise** (`mise.toml`): swift-format 602.0.0, SwiftLint 0.62.2, periphery 3.7.4. Run `mise install` once so `Scripts/lint.sh` can find them outside CI (not in Claude Code web sessions — see "Linux builds" for how tooling works there). `Scripts/lint.sh` is env-driven: `LINT_MODE` (`STRICT` adds `--strict`/`--configuration`; `NONE`/`INSTALL` short-circuit), `FORMAT_ONLY=1` skips lint+build, and outside CI it auto-formats in place before linting. ## Architecture @@ -36,6 +36,10 @@ Both store the same three fields: `baseKey`, a `styles` map (`ConfigKeySource -> - Every source file carries the MIT license header (copyright "Leo Dion" / "BrightDigit"); `Scripts/header.sh` enforces it. New files need it. - `periphery.yml` sets `retain_public: true`, so public API is never flagged as dead code. +## Linux builds + +This repo builds on Linux via SPM only — no Xcode, no Apple SDKs. The `platforms:` list in `Package.swift` applies to Apple platforms only and is ignored on Linux. **No targets are excluded on Linux**: both `ConfigKeyKit` and `ConfigKeyKitTests` build and test there (CI runs them in `swift:` containers). In Claude Code on the web, the SessionStart hook `.claude/hooks/session-start.sh` installs the toolchain via swiftly, pinned by `.swift-version` (requires `download.swift.org` on the environment's network allowlist), so `make lint` works too — with web-specific tooling: SwiftLint comes from its prebuilt Linux binary at the `mise.toml` pin (`mise install` cannot work in web sessions — the session's GitHub gateway scopes `api.github.com` to session-attached repos, so mise's release lookups 403; mise stays the install path for CI and local dev); swift-format is the one bundled with the Swift toolchain (its version tracks the toolchain, not the `mise.toml` pin — CI strict-lints with the pin, so if formatting disagrees with CI, that drift is why); periphery is not installed, and `Scripts/lint.sh` skips its scan when `CLAUDE_CODE_REMOTE` is set — run periphery locally to catch dead code. The hook runs **async**: the session starts immediately while installs continue in the background, so on a brand-new container `swift` can take a few minutes to appear — progress is in `~/.claude-session-setup.log`, and `~/.claude-session-setup.done` marks completion; wait for it before treating a missing tool as an error. Cached containers have everything instantly. + ## Note `ConfigKeyKit.git/` in the working tree is a bare git repo (a mirror clone), not part of the package — leave it alone. diff --git a/Scripts/lint.sh b/Scripts/lint.sh index e2e602b..322167c 100755 --- a/Scripts/lint.sh +++ b/Scripts/lint.sh @@ -52,8 +52,13 @@ fi $PACKAGE_DIR/Scripts/header.sh -d $PACKAGE_DIR/Sources -c "Leo Dion" -o "BrightDigit" -p "ConfigKeyKit" -if [ -z "$CI" ]; then +# Periphery does not run in Claude Code web sessions: it would have to be +# built from source there (no Linux binaries, and the session's GitHub +# gateway rules out mise), which is not worth the cold-start cost. +if [ -z "$CI" ] && [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then run_command periphery scan $PERIPHERY_OPTIONS --disable-update-check +elif [ "${CLAUDE_CODE_REMOTE:-}" = "true" ]; then + echo "Skipping periphery scan (Claude Code web session)." fi popd