#!/bin/bash # Accessibility Agents Installer # Built by Community Access - https://community-access.org # # Usage: # bash install.sh Interactive mode (prompts for project or global) # bash install.sh --global Install globally to ~/.claude/ # bash install.sh --global --copilot Also install Copilot agents to VS Code # bash install.sh --global --cli Also install Copilot CLI agents to ~/.copilot/ # bash install.sh --global --codex Also install Codex skills to ~/.codex/skills/ # bash install.sh --global --gemini Also install Gemini CLI extension # bash install.sh --project Install to .claude/ in the current directory # bash install.sh --project --copilot Also install Copilot agents to project # bash install.sh --project --cli Also install Copilot CLI agents to project # bash install.sh --project --codex Also install Codex skills to .codex/skills/ # bash install.sh --project --gemini Also install Gemini CLI extension # bash install.sh --global --vscode-stable Target VS Code stable only for Copilot assets # bash install.sh --global --vscode-insiders Target VS Code Insiders only for Copilot assets # bash install.sh --global --mcp-profile-both Configure MCP settings in both VS Code profiles # bash install.sh --yes Accept optional install prompts automatically # bash install.sh --no-auto-update Skip auto-update setup without prompting # bash install.sh --dry-run Preview targets without making changes # bash install.sh --summary=path.json Write a machine-readable summary file # # One-liner: # curl -fsSL https://raw.githubusercontent.com/Community-Access/accessibility-agents/main/install.sh | bash set -e # Determine source: running from repo clone or piped from curl? SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd)" DOWNLOADED=false if [ ! -d "$SCRIPT_DIR/claude-code-plugin/agents" ] && [ ! -d "$SCRIPT_DIR/.claude/agents" ]; then # Running from curl pipe or without repo — download first DOWNLOADED=true TMPDIR_DL="$(mktemp -d)" echo "" echo " Downloading Accessibility Agents..." if ! command -v git &>/dev/null; then echo " Error: git is required. Install git and try again." rm -rf "$TMPDIR_DL" exit 1 fi git clone --quiet https://github.com/Community-Access/accessibility-agents.git "$TMPDIR_DL/accessibility-agents" 2>/dev/null if [ $? -ne 0 ]; then echo " Error: git clone failed. Check your network connection and try again." rm -rf "$TMPDIR_DL" exit 1 fi SCRIPT_DIR="$TMPDIR_DL/accessibility-agents" echo " Downloaded." fi . "$SCRIPT_DIR/scripts/installer-common.sh" enforce_shell_runtime # Prefer claude-code-plugin/ as distribution source, fall back to .claude/agents/ if [ -d "$SCRIPT_DIR/claude-code-plugin/agents" ]; then AGENTS_SRC="$SCRIPT_DIR/claude-code-plugin/agents" else AGENTS_SRC="$SCRIPT_DIR/.claude/agents" fi if [ -d "$SCRIPT_DIR/claude-code-plugin/skills" ]; then SKILLS_SRC="$SCRIPT_DIR/claude-code-plugin/skills" elif [ -d "$SCRIPT_DIR/claude-code-plugin/commands" ]; then # Backwards compat: old repos may still have commands/ SKILLS_SRC="$SCRIPT_DIR/claude-code-plugin/commands" else SKILLS_SRC="" fi PLUGIN_CLAUDE_MD="" if [ -f "$SCRIPT_DIR/claude-code-plugin/CLAUDE.md" ]; then PLUGIN_CLAUDE_MD="$SCRIPT_DIR/claude-code-plugin/CLAUDE.md" fi # Plugin source and version for global installs PLUGIN_SRC="" PLUGIN_VERSION="1.0.0" if [ -d "$SCRIPT_DIR/claude-code-plugin/.claude-plugin" ]; then PLUGIN_SRC="$SCRIPT_DIR/claude-code-plugin" if command -v python3 &>/dev/null && [ -f "$PLUGIN_SRC/.claude-plugin/plugin.json" ]; then PLUGIN_VERSION=$(python3 -c "import json; print(json.load(open('$PLUGIN_SRC/.claude-plugin/plugin.json'))['version'])" 2>/dev/null || echo "1.0.0") fi fi COPILOT_AGENTS_SRC="$SCRIPT_DIR/.github/agents" COPILOT_CONFIG_SRC="$SCRIPT_DIR/.github" MCP_SERVER_SRC="$SCRIPT_DIR/mcp-server" # Auto-detect agents from source directory AGENTS=() if [ -d "$AGENTS_SRC" ]; then for f in "$AGENTS_SRC"/*.md; do [ -f "$f" ] && AGENTS+=("$(basename "$f")") done fi # Auto-detect skills from source directory SKILLS=() if [ -n "$SKILLS_SRC" ] && [ -d "$SKILLS_SRC" ]; then for f in "$SKILLS_SRC"/*.md; do [ -f "$f" ] && SKILLS+=("$(basename "$f")") done fi # Validate source files exist if [ ${#AGENTS[@]} -eq 0 ]; then echo " Error: No agents found in $AGENTS_SRC" echo " Make sure you are running this script from the accessibility-agents directory." [ "$DOWNLOADED" = true ] && rm -rf "$TMPDIR_DL" exit 1 fi # --------------------------------------------------------------------------- # migrate_prompts src_dir # Rename old prompt filenames to new agent-matching names. # This ensures users upgrading from v2.x to v3.0 don't lose custom prompts. # Migration: old naming (task-based) → new naming (agent-based) # --------------------------------------------------------------------------- migrate_prompts() { local src_dir="$1" [ -d "$src_dir" ] || return local -a migrations=( "a11y-update.prompt.md:insiders-a11y-tracker.prompt.md" "audit-desktop-a11y.prompt.md:desktop-a11y-specialist.prompt.md" "audit-markdown.prompt.md:markdown-a11y-assistant.prompt.md" "audit-web-page.prompt.md:web-accessibility-wizard.prompt.md" "export-document-csv.prompt.md:document-csv-reporter.prompt.md" "export-markdown-csv.prompt.md:markdown-csv-reporter.prompt.md" "export-web-csv.prompt.md:web-csv-reporter.prompt.md" "package-python-app.prompt.md:python-specialist.prompt.md" "review-text-quality.prompt.md:text-quality-reviewer.prompt.md" "scaffold-nvda-addon.prompt.md:nvda-addon-specialist.prompt.md" "scaffold-wxpython-app.prompt.md:wxpython-specialist.prompt.md" "test-desktop-a11y.prompt.md:desktop-a11y-testing-coach.prompt.md" ) for mapping in "${migrations[@]}"; do IFS=: read -r old_name new_name <<< "$mapping" local old_file="$src_dir/$old_name" local new_file="$src_dir/$new_name" if [ -f "$old_file" ] && [ ! -f "$new_file" ]; then mv "$old_file" "$new_file" 2>/dev/null || true elif [ -f "$old_file" ] && [ -f "$new_file" ]; then # Both exist; remove old version and keep new rm -f "$old_file" 2>/dev/null || true fi done } # Parse flags for non-interactive install choice="" COPILOT_FLAG=false COPILOT_CLI_FLAG=false CODEX_FLAG=false GEMINI_FLAG=false DRY_RUN=false CHECK_MODE=false SUMMARY_PATH="" VSCODE_PROFILE_MODE="auto" MCP_PROFILE_MODE="auto" AUTO_APPROVE=false NO_AUTO_UPDATE=false for arg in "$@"; do case "$arg" in --global) choice="2" ;; --project) choice="1" ;; --copilot) COPILOT_FLAG=true ;; --cli) COPILOT_CLI_FLAG=true ;; --codex) CODEX_FLAG=true ;; --gemini) GEMINI_FLAG=true ;; --yes) AUTO_APPROVE=true ;; --no-auto-update) NO_AUTO_UPDATE=true ;; --check) CHECK_MODE=true ;; --dry-run) DRY_RUN=true ;; --vscode-stable) VSCODE_PROFILE_MODE=$(set_profile_mode "$VSCODE_PROFILE_MODE" "stable") ;; --vscode-insiders) VSCODE_PROFILE_MODE=$(set_profile_mode "$VSCODE_PROFILE_MODE" "insiders") ;; --vscode-both) VSCODE_PROFILE_MODE=$(set_profile_mode "$VSCODE_PROFILE_MODE" "both") ;; --mcp-profile-stable) MCP_PROFILE_MODE=$(set_profile_mode "$MCP_PROFILE_MODE" "stable") ;; --mcp-profile-insiders) MCP_PROFILE_MODE=$(set_profile_mode "$MCP_PROFILE_MODE" "insiders") ;; --mcp-profile-both) MCP_PROFILE_MODE=$(set_profile_mode "$MCP_PROFILE_MODE" "both") ;; --summary=*) SUMMARY_PATH="${arg#--summary=}" ;; esac done OPTIONAL_PLATFORM_FLAGS=false if [ "$COPILOT_FLAG" = true ] || [ "$COPILOT_CLI_FLAG" = true ] || [ "$CODEX_FLAG" = true ] || [ "$GEMINI_FLAG" = true ]; then OPTIONAL_PLATFORM_FLAGS=true fi if [ -z "$choice" ]; then if ! has_tty; then echo " Error: choose either --project or --global when running non-interactively." exit 1 fi echo "" echo " Accessibility Agents Installer" echo " Built by Community Access" echo " ================================" echo "" echo " Where would you like to install?" echo "" echo " 1) Project - Install to .claude/ in the current directory" echo " (recommended, check into version control)" echo "" echo " 2) Global - Install to ~/.claude/" echo " (available in all your projects)" echo "" printf " Choose [1/2]: " read -r choice < /dev/tty fi case "$choice" in 1) TARGET_DIR="$(pwd)/.claude" echo "" echo " Installing to project: $(pwd)" ;; 2) TARGET_DIR="$HOME/.claude" echo "" echo " Installing globally to: $TARGET_DIR" ;; *) echo " Invalid choice. Exiting." [ "$DOWNLOADED" = true ] && rm -rf "$TMPDIR_DL" exit 1 ;; esac SELECTED_COPILOT_PROFILES="$(select_vscode_profiles "$VSCODE_PROFILE_MODE")" SELECTED_MCP_PROFILES="$(select_vscode_profiles "$MCP_PROFILE_MODE")" if [ -z "$SUMMARY_PATH" ]; then if [ "$DRY_RUN" = true ] || [ "$CHECK_MODE" = true ]; then SUMMARY_PATH="${HOME}/.a11y-agent-team-install-plan.json" elif [ "$choice" = "1" ]; then SUMMARY_PATH="$(pwd)/.a11y-agent-team-install-summary.json" else SUMMARY_PATH="$HOME/.a11y-agent-team-install-summary.json" fi fi BACKUP_METADATA_PATH="$(initialize_operation_state install "$([ "$choice" = "1" ] && pwd || printf '%s' "$HOME")" "$SUMMARY_PATH" "$DRY_RUN" "$CHECK_MODE" "$TARGET_DIR" "$TARGET_DIR/.a11y-agent-manifest" "$TARGET_DIR/.a11y-agent-team-version")" if [ "$CHECK_MODE" = true ]; then CHECK_NOTES=("Check mode only. No files were changed.") write_summary_file "$SUMMARY_PATH" "{\"schemaVersion\":\"1.0\",\"timestampUtc\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"operation\":\"install\",\"dryRun\":false,\"check\":true,\"scope\":\"$([ \"$choice\" = \"1\" ] && echo project || echo global)\",\"targetDir\":\"$(json_escape "$TARGET_DIR")\",\"requestedOptions\":{\"copilot\":$(json_bool "$COPILOT_FLAG"),\"copilotCli\":$(json_bool "$COPILOT_CLI_FLAG"),\"codex\":$(json_bool "$CODEX_FLAG"),\"gemini\":$(json_bool "$GEMINI_FLAG"),\"autoApprove\":$(json_bool "$AUTO_APPROVE"),\"noAutoUpdate\":$(json_bool "$NO_AUTO_UPDATE"),\"vscodeProfileMode\":\"$VSCODE_PROFILE_MODE\",\"mcpProfileMode\":\"$MCP_PROFILE_MODE\"},\"selectedCopilotProfiles\":$(json_array_from_profiles "$SELECTED_COPILOT_PROFILES" path),\"selectedMcpProfiles\":$(json_array_from_profiles "$SELECTED_MCP_PROFILES" settings),\"backupMetadataPath\":\"$(json_escape "$BACKUP_METADATA_PATH")\",\"notes\":$(json_array_from_notes "${CHECK_NOTES[@]}")}" echo "" echo " Check mode only. No files will be changed." echo " Summary file: $SUMMARY_PATH" echo " Backup metadata: $BACKUP_METADATA_PATH" [ "$DOWNLOADED" = true ] && rm -rf "$TMPDIR_DL" exit 0 fi if [ "$DRY_RUN" = true ]; then DRY_RUN_NOTES=() if [ "$AUTO_APPROVE" = true ]; then DRY_RUN_NOTES+=("Interactive prompts would be skipped because --yes was supplied.") fi if [ "$NO_AUTO_UPDATE" = true ]; then DRY_RUN_NOTES+=("Auto-update setup would be skipped because --no-auto-update was supplied.") fi if [ "$COPILOT_FLAG" = false ] && [ "$COPILOT_CLI_FLAG" = false ] && [ "$CODEX_FLAG" = false ] && [ "$GEMINI_FLAG" = false ]; then DRY_RUN_NOTES+=("Optional platforms were not selected in dry-run mode. Use --copilot, --cli, --codex, and/or --gemini to preview them explicitly.") fi echo "" echo " Dry run only. No files will be changed." echo " Scope: $([ "$choice" = "1" ] && echo project || echo global)" echo " Target: $TARGET_DIR" if [ "$choice" = "2" ]; then echo " VS Code profiles in scope:" if [ -n "$SELECTED_COPILOT_PROFILES" ]; then while IFS='|' read -r key label path; do [ -n "$path" ] && echo " -> $label: $path" done <<< "$SELECTED_COPILOT_PROFILES" else echo " -> none detected for the requested profile filter" fi echo " MCP settings targets:" if [ -n "$SELECTED_MCP_PROFILES" ]; then while IFS='|' read -r key label path; do [ -n "$path" ] && echo " -> $label: $path/settings.json" done <<< "$SELECTED_MCP_PROFILES" else echo " -> none detected for the requested profile filter" fi fi write_summary_file "$SUMMARY_PATH" "{\"schemaVersion\":\"1.0\",\"timestampUtc\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"operation\":\"install\",\"dryRun\":true,\"check\":false,\"scope\":\"$([ \"$choice\" = \"1\" ] && echo project || echo global)\",\"targetDir\":\"$(json_escape "$TARGET_DIR")\",\"requestedOptions\":{\"copilot\":$(json_bool "$COPILOT_FLAG"),\"copilotCli\":$(json_bool "$COPILOT_CLI_FLAG"),\"codex\":$(json_bool "$CODEX_FLAG"),\"gemini\":$(json_bool "$GEMINI_FLAG"),\"autoApprove\":$(json_bool "$AUTO_APPROVE"),\"noAutoUpdate\":$(json_bool "$NO_AUTO_UPDATE"),\"vscodeProfileMode\":\"$VSCODE_PROFILE_MODE\",\"mcpProfileMode\":\"$MCP_PROFILE_MODE\"},\"selectedCopilotProfiles\":$(json_array_from_profiles "$SELECTED_COPILOT_PROFILES" path),\"selectedMcpProfiles\":$(json_array_from_profiles "$SELECTED_MCP_PROFILES" settings),\"backupMetadataPath\":\"$(json_escape "$BACKUP_METADATA_PATH")\",\"notes\":$(json_array_from_notes "${DRY_RUN_NOTES[@]}")}" echo " Summary file: $SUMMARY_PATH" [ "$DOWNLOADED" = true ] && rm -rf "$TMPDIR_DL" exit 0 fi # --------------------------------------------------------------------------- # merge_config_file src dst label # Appends/updates our section in a config markdown file using section markers. # Never overwrites user content above or below our section. # --------------------------------------------------------------------------- merge_config_file() { local src="$1" dst="$2" label="$3" local start end legacy_start legacy_end case "$dst" in *.toml) start="# a11y-agent-team: start" end="# a11y-agent-team: end" legacy_start="# accessibility-agents: start" legacy_end="# accessibility-agents: end" ;; *) start="" end="" legacy_start="" legacy_end="" ;; esac if [ ! -f "$dst" ]; then { printf '%s\n' "$start"; cat "$src"; printf '%s\n' "$end"; } > "$dst" echo " + $label (created)" return fi if grep -qF "$start" "$dst" 2>/dev/null || grep -qF "$legacy_start" "$dst" 2>/dev/null; then if command -v python3 &>/dev/null; then python3 - "$src" "$dst" "$start" "$end" "$legacy_start" "$legacy_end" << 'PYEOF' import re, sys src_text = open(sys.argv[1]).read().rstrip() dst_path = sys.argv[2] dst_text = open(dst_path).read() start = sys.argv[3] end = sys.argv[4] legacy_start = sys.argv[5] legacy_end = sys.argv[6] block = start + "\n" + src_text + "\n" + end patterns = [re.escape(start) + r".*?" + re.escape(end)] if legacy_start and legacy_end: patterns.append(re.escape(legacy_start) + r".*?" + re.escape(legacy_end)) combined = r"(?s)(?:" + "|".join(patterns) + r")" m = re.search(combined, dst_text) if m: insert_pos = m.start() cleaned = re.sub(combined, "", dst_text) updated = cleaned[:insert_pos] + block + cleaned[insert_pos:] else: updated = dst_text open(dst_path, "w").write(updated) PYEOF echo " ~ $label (updated our existing section)" else echo " ! $label (section exists; python3 unavailable to update - edit manually)" fi else # For TOML files: guard against inserting duplicate table headers _toml_dup="" case "$dst" in *.toml) if command -v python3 &>/dev/null; then _toml_dup=$(python3 -c 'import re, sys; hdr_re = re.compile(r"^\[[^]]+\]", re.MULTILINE); src_hdrs = set(hdr_re.findall(open(sys.argv[1]).read())); dst_hdrs = set(hdr_re.findall(open(sys.argv[2]).read())); dupes = src_hdrs & dst_hdrs; print(",".join(sorted(dupes))) if dupes else None' "$src" "$dst" 2>/dev/null || true) fi ;; esac if [ -n "$_toml_dup" ]; then echo " ! $label (skipped — TOML table headers already exist in destination: $_toml_dup)" else { printf '\n%s\n' "$start"; cat "$src"; printf '%s\n' "$end"; echo; } >> "$dst" echo " + $label (merged into your existing file)" fi fi } # --------------------------------------------------------------------------- # configure_vscode_mcp_settings settings_file server_url # Adds or updates the VS Code MCP server entry for a11y-agent-team. # --------------------------------------------------------------------------- configure_vscode_mcp_settings() { local settings_file="$1" local server_url="$2" mkdir -p "$(dirname "$settings_file")" if command -v python3 &>/dev/null; then if A11Y_SF="$settings_file" A11Y_MCP_URL="$server_url" python3 - << 'PYEOF' 2>/dev/null import json, os, sys sf = os.environ['A11Y_SF'] url = os.environ['A11Y_MCP_URL'] try: if os.path.exists(sf): raw = open(sf, 'r', encoding='utf-8').read().strip() data = json.loads(raw) if raw else {} else: data = {} except Exception: sys.exit(2) mcp = data.setdefault('mcp', {}) servers = mcp.setdefault('servers', {}) servers['a11y-agent-team'] = {'url': url} with open(sf, 'w', encoding='utf-8') as f: json.dump(data, f, indent=4) PYEOF then echo " + MCP server configured in $settings_file" return 0 fi echo " ! Could not safely update $settings_file" else echo " ! python3 not found, so VS Code settings were not edited automatically" fi echo " Add this manually: { \"mcp\": { \"servers\": { \"a11y-agent-team\": { \"url\": \"$server_url\" } } } }" return 1 } node_major_version() { command -v node &>/dev/null || return 1 node -p "process.versions.node.split('.')[0]" 2>/dev/null } java_major_version() { command -v java &>/dev/null || return 1 local java_line java_line="$(java -version 2>&1 | head -n 1)" if [[ "$java_line" =~ \"([0-9]+)\.([0-9]+)\" ]]; then if [ "${BASH_REMATCH[1]}" = "1" ]; then echo "${BASH_REMATCH[2]}" else echo "${BASH_REMATCH[1]}" fi return 0 fi if [[ "$java_line" =~ \"([0-9]+)\" ]]; then echo "${BASH_REMATCH[1]}" return 0 fi if [[ "$java_line" =~ ([0-9]+) ]]; then echo "${BASH_REMATCH[1]}" return 0 fi return 1 } ensure_nodejs_runtime() { local node_major="" if command -v node &>/dev/null && command -v npm &>/dev/null; then node_major="$(node_major_version || true)" if [ -n "$node_major" ] && [ "$node_major" -ge 18 ]; then return 0 fi fi echo "" if command -v node &>/dev/null; then node_major="$(node_major_version || true)" if [ -n "$node_major" ]; then echo " Detected Node.js $node_major, but the MCP server requires Node.js 18 or later." else echo " Node.js is installed, but its version could not be verified." fi else echo " Node.js and npm were not found." fi if command -v brew &>/dev/null; then if has_tty || [ "$AUTO_APPROVE" = true ]; then echo " The installer can install Node.js using Homebrew." if read_yes_no "Install Node.js now?" true; then brew install node || true fi fi else echo " Automatic Node.js install is only supported by this shell installer on macOS via Homebrew." fi if command -v node &>/dev/null && command -v npm &>/dev/null; then node_major="$(node_major_version || true)" if [ -n "$node_major" ] && [ "$node_major" -ge 18 ]; then echo " + Node.js runtime is ready for the MCP server" return 0 fi fi echo " MCP setup can continue, but scanning will remain unavailable until Node.js 18+ and npm are installed." echo " Manual fallback: https://nodejs.org/en/download" echo " After installing Node.js, reopen your terminal and run:" echo " cd \"$MCP_DEST\" && npm install" return 1 } read_yes_no() { local prompt="$1" local default_yes="$2" local reply="" local suffix="[y/N]" [ "$default_yes" = true ] && suffix="[Y/n]" if [ "$AUTO_APPROVE" = true ]; then return 0 fi if has_tty; then printf " %s %s: " "$prompt" "$suffix" read -r reply < /dev/tty else [ "$default_yes" = true ] return fi if [ -z "$reply" ]; then [ "$default_yes" = true ] return fi [ "$reply" = "y" ] || [ "$reply" = "Y" ] } choose_mcp_capability_plan() { MCP_PLAN_FOCUS="Baseline scanning" MCP_PLAN_BROWSER=false MCP_PLAN_PDF_FORMS=false MCP_PLAN_DEEP_PDF=false MCP_PLAN_CONFIGURE_VSCODE=true if [ "$AUTO_APPROVE" = true ] || ! has_tty; then return fi echo "" echo " Choose your MCP setup focus:" echo "" echo " 1) Baseline scanning" echo " 2) Browser testing" echo " 3) PDF-heavy workflow" echo " 4) Everything" echo " 5) Custom" echo "" printf " Choose [1/2/3/4/5]: " read -r mcp_plan_choice < /dev/tty case "$mcp_plan_choice" in 1) MCP_PLAN_FOCUS="Baseline scanning" ;; 2) MCP_PLAN_FOCUS="Browser testing" MCP_PLAN_BROWSER=true ;; 3) MCP_PLAN_FOCUS="PDF-heavy workflow" MCP_PLAN_PDF_FORMS=true MCP_PLAN_DEEP_PDF=true ;; 4) MCP_PLAN_FOCUS="Everything" MCP_PLAN_BROWSER=true MCP_PLAN_PDF_FORMS=true MCP_PLAN_DEEP_PDF=true ;; 5) MCP_PLAN_FOCUS="Custom" read_yes_no "Enable browser tools?" false && MCP_PLAN_BROWSER=true read_yes_no "Enable PDF form tools?" false && MCP_PLAN_PDF_FORMS=true read_yes_no "Enable deep PDF validation?" false && MCP_PLAN_DEEP_PDF=true read_yes_no "Configure VS Code MCP?" true && MCP_PLAN_CONFIGURE_VSCODE=true ;; *) MCP_PLAN_FOCUS="Baseline scanning" ;; esac } show_mcp_capability_warnings() { echo "" echo " MCP capability plan: $MCP_PLAN_FOCUS" echo " - Baseline scanning installs the MCP server plus core npm dependencies" if [ "$MCP_PLAN_BROWSER" = true ]; then echo " - Browser testing needs Playwright, axe-core, and Chromium" echo " - Browser scans run against live pages and can take longer to install" fi if [ "$MCP_PLAN_PDF_FORMS" = true ]; then echo " - PDF form conversion needs the optional pdf-lib package" fi if [ "$MCP_PLAN_DEEP_PDF" = true ]; then echo " - Deep PDF validation needs Java 11+ and veraPDF" echo " - Baseline PDF scanning still works even if deep validation is not ready" fi echo " - Python is not required for MCP runtime" echo " - macOS is supported by this shell installer; Linux is not part of the guided installer target" } # --------------------------------------------------------------------------- # show_pdf_deep_validation_readiness # Prints whether Java and veraPDF are available for run_verapdf_scan. # --------------------------------------------------------------------------- show_pdf_deep_validation_readiness() { local java_line="" local verapdf_line="" local java_ok=false local verapdf_ok=false local java_major="" if command -v java &>/dev/null; then java_ok=true java_line="$(java -version 2>&1 | head -n 1)" java_major="$(java_major_version || true)" fi if command -v verapdf &>/dev/null; then verapdf_ok=true verapdf_line="$(verapdf --version 2>&1 | head -n 1)" fi echo "" echo " PDF Deep Validation Readiness:" if [ "$java_ok" = true ]; then if [ -n "$java_line" ]; then if [ -n "$java_major" ] && [ "$java_major" -ge 11 ]; then echo " [x] Java detected: $java_line" else echo " [!] Java detected but too old: $java_line" fi else if [ -n "$java_major" ] && [ "$java_major" -ge 11 ]; then echo " [x] Java command found" else echo " [!] Java command found, but version could not be confirmed as 11+" fi fi else echo " [ ] Java not detected" fi if [ "$verapdf_ok" = true ]; then if [ -n "$verapdf_line" ]; then echo " [x] veraPDF detected: $verapdf_line" else echo " [x] veraPDF command found" fi else echo " [ ] veraPDF not detected" fi if [ "$java_ok" = true ] && [ -n "$java_major" ] && [ "$java_major" -ge 11 ] && [ "$verapdf_ok" = true ]; then echo " READY: run_verapdf_scan should be available once the MCP server is running." elif [ "$java_ok" = true ] && [ -n "$java_major" ] && [ "$java_major" -ge 11 ]; then echo " PARTIAL: Java is ready, but veraPDF still needs to be installed." elif [ "$java_ok" = true ]; then echo " NOT READY: Java 11 or later is required before veraPDF can run." else echo " NOT READY: scan_pdf_document will work, but run_verapdf_scan will not yet be available." fi } mcp_health_smoke_test() { local working_dir="$1" local port="" local log_file="" local pid="" local success=false if ! command -v node &>/dev/null || ! command -v npm &>/dev/null; then echo "[ ] SKIPPED|Baseline MCP prerequisites are not fully installed yet." return 0 fi local node_major="$(node_major_version || true)" if [ -z "$node_major" ] || [ "$node_major" -lt 18 ]; then echo "[ ] SKIPPED|Baseline MCP prerequisites are not fully installed yet." return 0 fi if ! node_module_available "$working_dir" "@modelcontextprotocol/sdk" || ! node_module_available "$working_dir" "zod"; then echo "[ ] SKIPPED|Baseline MCP prerequisites are not fully installed yet." return 0 fi port=$((4300 + RANDOM % 200)) log_file="$(mktemp)" ( cd "$working_dir" && \ PORT="$port" A11Y_MCP_HOST=127.0.0.1 A11Y_MCP_STATELESS=1 node server.js ) >"$log_file" 2>&1 & pid=$! for _ in $(seq 1 20); do sleep 1 if command -v curl &>/dev/null; then if curl -fsS "http://127.0.0.1:$port/health" 2>/dev/null | grep -q '"status":"ok"'; then success=true break fi elif command -v python3 &>/dev/null; then if python3 - </dev/null 2>&1 import json, sys, urllib.request port = sys.argv[1] with urllib.request.urlopen(f'http://127.0.0.1:{port}/health', timeout=2) as response: data = json.load(response) raise SystemExit(0 if data.get('status') == 'ok' else 1) PYEOF then success=true break fi fi done if [ -n "$pid" ]; then kill "$pid" 2>/dev/null || true wait "$pid" 2>/dev/null || true fi if [ "$success" = true ]; then rm -f "$log_file" echo "[x] READY|HTTP health check passed on port $port." return 0 fi local detail="The temporary MCP server did not answer /health in time." if [ -f "$log_file" ]; then local first_line first_line="$(head -n 1 "$log_file")" if [ -n "$first_line" ]; then detail="$first_line" fi fi rm -f "$log_file" echo "[ ] FAILED|$detail" } # --------------------------------------------------------------------------- # node_module_available working_dir module_name # Returns success if the Node module can be imported from the given directory. # --------------------------------------------------------------------------- node_module_available() { local working_dir="$1" local module_name="$2" command -v node &>/dev/null || return 1 [ -n "$working_dir" ] && [ -d "$working_dir" ] || return 1 (cd "$working_dir" && node -e "import('$module_name').then(() => process.exit(0)).catch(() => process.exit(1))" >/dev/null 2>&1) } # --------------------------------------------------------------------------- # playwright_chromium_ready working_dir # Returns success if Playwright can resolve an installed Chromium executable. # --------------------------------------------------------------------------- playwright_chromium_ready() { local working_dir="$1" command -v node &>/dev/null || return 1 [ -n "$working_dir" ] && [ -d "$working_dir" ] || return 1 (cd "$working_dir" && node -e "import('playwright').then(async ({ chromium }) => { const fs = await import('node:fs'); const exe = chromium.executablePath(); process.exit(exe && fs.existsSync(exe) ? 0 : 1); }).catch(() => process.exit(1))" >/dev/null 2>&1) } # --------------------------------------------------------------------------- # show_mcp_capability_readiness working_dir # Prints readiness for optional MCP capabilities. # --------------------------------------------------------------------------- show_mcp_capability_readiness() { local working_dir="$1" local node_ready=false local npm_ready=false local core_ready=false local python_ready=false local java_ready=false local verapdf_ready=false local playwright_ready=false local chromium_ready=false local pdf_lib_ready=false local node_major="" local java_major="" local smoke_result="" local smoke_label="" local smoke_detail="" if command -v node &>/dev/null; then node_major="$(node_major_version || true)" if [ -n "$node_major" ] && [ "$node_major" -ge 18 ]; then node_ready=true fi fi command -v npm &>/dev/null && npm_ready=true if node_module_available "$working_dir" "@modelcontextprotocol/sdk" && node_module_available "$working_dir" "zod"; then core_ready=true fi command -v python3 &>/dev/null && python_ready=true if command -v java &>/dev/null; then java_major="$(java_major_version || true)" if [ -n "$java_major" ] && [ "$java_major" -ge 11 ]; then java_ready=true fi fi command -v verapdf &>/dev/null && verapdf_ready=true node_module_available "$working_dir" "playwright" && playwright_ready=true node_module_available "$working_dir" "pdf-lib" && pdf_lib_ready=true playwright_chromium_ready "$working_dir" && chromium_ready=true smoke_result="$(mcp_health_smoke_test "$working_dir")" smoke_label="${smoke_result%%|*}" smoke_detail="${smoke_result#*|}" echo "" echo " MCP Optional Capability Readiness:" if [ -n "$node_major" ]; then if [ "$node_ready" = true ]; then echo " Node.js runtime (18+): [x] READY (v$node_major)" else echo " Node.js runtime (18+): [!] TOO OLD (v$node_major)" fi else echo " Node.js runtime (18+): [ ] NOT READY" fi echo " npm CLI: $([ "$npm_ready" = true ] && echo '[x] READY' || echo '[ ] NOT READY')" echo " MCP core dependencies: $([ "$core_ready" = true ] && echo '[x] READY' || echo '[ ] NOT READY')" echo " Python 3 helper (installer only): $([ "$python_ready" = true ] && echo '[x] OPTIONAL' || echo '[ ] OPTIONAL')" if [ "$node_ready" = true ] && [ "$npm_ready" = true ] && [ "$core_ready" = true ]; then echo " Baseline PDF scan (scan_pdf_document): [x] READY" else echo " Baseline PDF scan (scan_pdf_document): [ ] NOT READY" fi if [ -n "$java_major" ]; then if [ "$java_ready" = true ]; then echo " Deep PDF validation (Java 11+): [x] READY (v$java_major)" else echo " Deep PDF validation (Java 11+): [!] TOO OLD (v$java_major)" fi else echo " Deep PDF validation (Java 11+): [ ] NOT READY" fi echo " Deep PDF validation (veraPDF): $([ "$verapdf_ready" = true ] && echo '[x] READY' || echo '[ ] NOT READY')" echo " Local MCP health smoke test: $smoke_label" echo " Playwright package: $([ "$playwright_ready" = true ] && echo '[x] READY' || echo '[ ] NOT READY')" echo " Chromium browser bundle: $([ "$chromium_ready" = true ] && echo '[x] READY' || echo '[ ] NOT READY')" echo " PDF form conversion (pdf-lib): $([ "$pdf_lib_ready" = true ] && echo '[x] READY' || echo '[ ] NOT READY')" if [ "$node_ready" != true ] || [ "$npm_ready" != true ] || [ "$core_ready" != true ]; then echo " Baseline scanning needs Node.js 18+, npm, and MCP server dependencies in the MCP directory." fi if [ "$python_ready" != true ]; then echo " Python 3 is not required for MCP runtime. Without it, some shell-installer config edits fall back to manual steps." fi if [ -n "$smoke_detail" ]; then echo " Smoke test detail: $smoke_detail" fi if [ "$playwright_ready" != true ] || [ "$chromium_ready" != true ]; then echo " Browser-based scans need Playwright plus Chromium." fi if [ "$pdf_lib_ready" != true ]; then echo " PDF form conversion needs pdf-lib in the MCP server directory." fi } # --------------------------------------------------------------------------- # register_plugin src_dir # Registers accessibility-agents as a Claude Code plugin for global installs. # Copies to plugin cache, updates installed_plugins.json and settings.json. # --------------------------------------------------------------------------- register_plugin() { local src="$1" local namespace="community-access" local name="accessibility-agents" local default_key="${name}@${namespace}" local plugins_json="$HOME/.claude/plugins/installed_plugins.json" local settings_json="$HOME/.claude/settings.json" echo "" echo " Registering Claude Code plugin..." # Ensure plugins directory exists mkdir -p "$HOME/.claude/plugins" local known_json="$HOME/.claude/plugins/known_marketplaces.json" local actual_key="$default_key" # ---- Step 1: Register the community-access marketplace ---- # Claude Code only loads plugins from known marketplaces. # Without this, the plugin is silently skipped. if command -v python3 &>/dev/null; then python3 - "$known_json" "$namespace" "$src" << 'PYEOF' import json, sys, os, datetime known_path, ns, plugin_src = sys.argv[1:4] # Read existing or create new if os.path.isfile(known_path): with open(known_path) as f: data = json.load(f) else: data = {} if ns not in data: # Create a marketplace directory alongside the plugin source marketplace_dir = os.path.join(os.path.dirname(plugin_src), ns + "-plugins") plugin_dir = os.path.join(marketplace_dir, "plugins") manifest_dir = os.path.join(marketplace_dir, ".claude-plugin") os.makedirs(plugin_dir, exist_ok=True) os.makedirs(manifest_dir, exist_ok=True) # Create marketplace.json if missing manifest_path = os.path.join(manifest_dir, "marketplace.json") if not os.path.isfile(manifest_path): manifest = { "name": ns, "owner": {"name": "Community Access"}, "metadata": {"description": "Accessibility-focused Claude Code plugins"}, "plugins": [{ "name": "accessibility-agents", "source": "./plugins/accessibility-agents", "description": "WCAG AA accessibility enforcement with 55 agents and enforcement hooks.", "version": "1.0.0" }] } with open(manifest_path, 'w') as f: json.dump(manifest, f, indent=2) # Symlink the plugin source into the marketplace link_target = os.path.join(plugin_dir, "accessibility-agents") if not os.path.exists(link_target): os.symlink(plugin_src, link_target) # Register marketplace now = datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.000Z') data[ns] = { "source": {"source": "directory", "path": marketplace_dir}, "installLocation": marketplace_dir, "lastUpdated": now } with open(known_path, 'w') as f: json.dump(data, f, indent=2) print(" + Registered community-access marketplace") else: print(" ~ community-access marketplace already registered") PYEOF else echo " ! python3 required for marketplace registration" fi # ---- Step 2: Detect existing registration (for upgrades) ---- if [ -f "$plugins_json" ] && command -v python3 &>/dev/null; then local found_key found_key=$(python3 -c " import json data = json.load(open('$plugins_json')) for k in data.get('plugins', {}): if k.startswith('accessibility-agents@'): print(k) break " 2>/dev/null) if [ -n "$found_key" ]; then actual_key="$found_key" namespace="${actual_key#accessibility-agents@}" fi fi local cache="$HOME/.claude/plugins/cache/${namespace}/${name}/${PLUGIN_VERSION}" # ---- Step 3: Copy plugin to cache ---- mkdir -p "$cache" cp -R "$src/." "$cache/" chmod +x "$cache/scripts/"*.sh 2>/dev/null || true echo " + Plugin cached" # ---- Step 4: Register in installed_plugins.json ---- if [ ! -f "$plugins_json" ]; then echo '{"version": 2, "plugins": {}}' > "$plugins_json" fi python3 - "$plugins_json" "$actual_key" "$cache" "$PLUGIN_VERSION" << 'PYEOF' import json, sys, datetime path, key, install_path, version = sys.argv[1:5] with open(path) as f: data = json.load(f) now = datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.000Z') data.setdefault('version', 2) data.setdefault('plugins', {})[key] = [{ "scope": "user", "installPath": install_path, "version": version, "installedAt": now, "lastUpdated": now }] with open(path, 'w') as f: json.dump(data, f, indent=2) PYEOF echo " + Registered in installed_plugins.json ($actual_key)" # ---- Step 5: Enable in settings.json ---- if [ ! -f "$settings_json" ]; then echo '{}' > "$settings_json" fi python3 - "$settings_json" "$actual_key" << 'PYEOF' import json, sys path, key = sys.argv[1:3] with open(path) as f: data = json.load(f) data.setdefault('enabledPlugins', {})[key] = True with open(path, 'w') as f: json.dump(data, f, indent=2) PYEOF echo " + Enabled in settings.json" # ---- Step 6: Clean up stale skills/ directory from previous installs ---- if [ -d "$cache/skills" ]; then rm -rf "$cache/skills" echo " ~ Removed stale skills/ directory" fi # Summary local agent_count cmd_count agent_count=$(ls "$cache/agents/"*.md 2>/dev/null | wc -l) cmd_count=$(ls "$cache/commands/"*.md 2>/dev/null | wc -l) echo "" echo " Plugin registered: $actual_key (v${PLUGIN_VERSION})" echo " $agent_count agents" echo " $cmd_count commands" echo " 3 enforcement hooks (UserPromptSubmit, PreToolUse, PostToolUse)" } # --------------------------------------------------------------------------- # cleanup_old_install # Removes agents/commands/skills from ~/.claude/ that were installed by a # previous non-plugin install (using the manifest file). # --------------------------------------------------------------------------- cleanup_old_install() { local manifest="$HOME/.claude/.a11y-agent-manifest" [ -f "$manifest" ] || return 0 echo "" echo " Cleaning up previous non-plugin install..." local removed=0 while IFS= read -r entry; do [ -n "$entry" ] || continue local file="$HOME/.claude/$entry" if [ -f "$file" ]; then rm "$file" removed=$((removed + 1)) fi done < "$manifest" rm -f "$manifest" rmdir "$HOME/.claude/agents" 2>/dev/null || true rmdir "$HOME/.claude/commands" 2>/dev/null || true rmdir "$HOME/.claude/skills" 2>/dev/null || true if [ "$removed" -gt 0 ]; then echo " Removed $removed files from previous install" fi } # --------------------------------------------------------------------------- # install_global_hooks # Installs three enforcement hooks: # 1. a11y-team-eval.sh (UserPromptSubmit) — Proactive web project detection # 2. a11y-enforce-edit.sh (PreToolUse) — Blocks UI file edits without review # 3. a11y-mark-reviewed.sh (PostToolUse) — Creates session marker after review # Merges all three into ~/.claude/settings.json. # --------------------------------------------------------------------------- install_global_hooks() { local hooks_dir="$HOME/.claude/hooks" local settings_json="$HOME/.claude/settings.json" mkdir -p "$hooks_dir" # ── Hook 1: Proactive web project detection (UserPromptSubmit) ── cat > "$hooks_dir/a11y-team-eval.sh" << 'HOOKSCRIPT' #!/bin/bash # Accessibility Agents - UserPromptSubmit hook # Two detection modes: # 1. PROACTIVE: Detects web projects by checking for framework files # 2. KEYWORD: Falls back to keyword matching for non-web projects # Installed by: accessibility-agents install.sh INPUT=$(cat) PROMPT=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('prompt','').lower())" 2>/dev/null || echo "") # ── PROACTIVE DETECTION ── IS_WEB_PROJECT=false if [ -f "package.json" ]; then if grep -qiE '"(react|next|vue|nuxt|svelte|sveltekit|astro|angular|gatsby|remix|solid|qwik|vite|webpack|parcel|tailwindcss|@emotion|styled-components|sass|less)"' package.json 2>/dev/null; then IS_WEB_PROJECT=true fi fi if [ "$IS_WEB_PROJECT" = false ]; then for f in next.config.js next.config.mjs next.config.ts nuxt.config.ts vite.config.ts vite.config.js svelte.config.js astro.config.mjs angular.json tailwind.config.js tailwind.config.ts postcss.config.js postcss.config.mjs tsconfig.json; do if [ -f "$f" ]; then IS_WEB_PROJECT=true break fi done fi if [ "$IS_WEB_PROJECT" = false ]; then if find . -maxdepth 3 -type f \( -name "*.jsx" -o -name "*.tsx" -o -name "*.vue" -o -name "*.svelte" -o -name "*.astro" \) -print -quit 2>/dev/null | grep -q .; then IS_WEB_PROJECT=true fi fi if [ "$IS_WEB_PROJECT" = false ]; then if find . -maxdepth 3 -type f \( -name "*.html" -o -name "*.ejs" -o -name "*.hbs" -o -name "*.leaf" -o -name "*.erb" -o -name "*.jinja" -o -name "*.twig" -o -name "*.blade.php" \) -print -quit 2>/dev/null | grep -q .; then IS_WEB_PROJECT=true fi fi # ── KEYWORD DETECTION ── KEYWORDS="html|jsx|tsx|css|tailwind|react|next\.?js|vue|svelte|astro|angular|component|widget|form|modal|dialog|popover|drawer|toast|notification|aria|wcag|a11y|accessibility|accessible|contrast|focus|keyboard|tab order|tabindex|screen ?reader|voiceover|nvda|jaws|talkback|heading|alt text|alt=|landmark|template|leaf|ejs|erb|hbs|jinja|twig|blade|vapor|render|view|page|button|input|select|checkbox|radio|label|dropdown|menu|accordion|carousel|slider|tooltip|card|sidebar|navbar|layout|grid|flex|responsive|theme|dark mode|color|style|font|image|img|svg|icon|video|audio|link|href|navigation|breadcrumb|pagination|loading|spinner|progress|alert|banner|role=|tabpanel|tablist|listbox|combobox|live region|aria-live|aria-label|aria-hidden|sr-only|visually-hidden|skip link|ui|ux|front.?end|client.?side|browser|dom|hydrat|ssr|ssg|not announcing|not reading|not saying|cannot hear|broken announce|announce" KEYWORD_MATCH=false if echo "$PROMPT" | grep -qiE "$KEYWORDS"; then KEYWORD_MATCH=true fi # ── OUTPUT ── if [ "$IS_WEB_PROJECT" = true ] || [ "$KEYWORD_MATCH" = true ]; then if [ "$IS_WEB_PROJECT" = true ]; then PROJECT_NOTE="DETECTED: This is a web project. Accessibility agents are ALWAYS required here." else PROJECT_NOTE="DETECTED: UI-related keywords found in prompt." fi cat < swift-agent-team:swift-lead handles Swift - HTML/templates/components -> accessibility-agents:accessibility-lead handles web - Both leads can run in parallel via separate Agent tool calls. HOOK_OUTPUT fi HOOKSCRIPT # ── Hook 2: Edit gate (PreToolUse on Edit|Write) ── cat > "$hooks_dir/a11y-enforce-edit.sh" << 'HOOKSCRIPT' #!/bin/bash # Accessibility Agents - PreToolUse enforcement hook # BLOCKS Edit/Write to UI files until accessibility-lead is consulted. # Uses permissionDecision: "deny" to reject the tool call. # Installed by: accessibility-agents install.sh INPUT=$(cat) eval "$(echo "$INPUT" | python3 -c " import sys, json data = json.load(sys.stdin) ti = data.get('tool_input', {}) print('FILE_PATH=' + repr(ti.get('file_path', ''))) print('SESSION_ID=' + repr(data.get('session_id', ''))) " 2>/dev/null || echo "FILE_PATH=''; SESSION_ID=''")" if [ -z "$FILE_PATH" ]; then exit 0 fi IS_UI=false case "$FILE_PATH" in *.jsx|*.tsx|*.vue|*.svelte|*.astro|*.html|*.ejs|*.hbs|*.leaf|*.erb|*.jinja|*.twig|*.blade.php) IS_UI=true ;; *.css|*.scss|*.less|*.sass) IS_UI=true ;; esac if [ "$IS_UI" = false ]; then case "$FILE_PATH" in */components/*|*/pages/*|*/views/*|*/layouts/*|*/templates/*) case "$FILE_PATH" in *.ts|*.js) IS_UI=true ;; esac ;; esac fi if [ "$IS_UI" = false ]; then exit 0 fi MARKER="/tmp/a11y-reviewed-${SESSION_ID}" if [ -n "$SESSION_ID" ] && [ -f "$MARKER" ]; then exit 0 fi BASENAME=$(basename "$FILE_PATH") cat < "$hooks_dir/a11y-mark-reviewed.sh" << 'HOOKSCRIPT' #!/bin/bash # Accessibility Agents - PostToolUse hook for Agent tool # Creates a session marker when accessibility-lead has been consulted. # This marker unlocks the a11y-enforce-edit.sh PreToolUse block. # Installed by: accessibility-agents install.sh INPUT=$(cat) eval "$(echo "$INPUT" | python3 -c " import sys, json data = json.load(sys.stdin) ti = data.get('tool_input', {}) subagent = ti.get('subagent_type', '') session_id = data.get('session_id', '') print('SUBAGENT=' + repr(subagent)) print('SESSION_ID=' + repr(session_id)) " 2>/dev/null || echo "SUBAGENT=''; SESSION_ID=''")" if [ -n "$SESSION_ID" ]; then case "$SUBAGENT" in *accessibility-lead*|*accessibility-agents:accessibility-lead*) touch "/tmp/a11y-reviewed-${SESSION_ID}" ;; esac fi exit 0 HOOKSCRIPT chmod +x "$hooks_dir/a11y-team-eval.sh" chmod +x "$hooks_dir/a11y-enforce-edit.sh" chmod +x "$hooks_dir/a11y-mark-reviewed.sh" # ── Register all three hooks in settings.json ── if [ ! -f "$settings_json" ]; then echo '{}' > "$settings_json" fi python3 - "$settings_json" "$hooks_dir" << 'PYEOF' import json, sys settings_path = sys.argv[1] hooks_dir = sys.argv[2] with open(settings_path) as f: data = json.load(f) hooks = data.setdefault("hooks", {}) # --- Helper: upsert a hook entry by matching a substring in the command --- def upsert_hook(event_name, match_substr, new_entry): event_hooks = hooks.setdefault(event_name, []) replaced = False for i, entry in enumerate(event_hooks): for h in entry.get("hooks", []): if match_substr in h.get("command", ""): event_hooks[i] = new_entry replaced = True break if replaced: break if not replaced: event_hooks.append(new_entry) # Hook 1: UserPromptSubmit — a11y-team-eval.sh upsert_hook("UserPromptSubmit", "a11y-team-eval", { "hooks": [{"type": "command", "command": hooks_dir + "/a11y-team-eval.sh"}] }) # Hook 2: PreToolUse — a11y-enforce-edit.sh (matcher: Edit|Write) upsert_hook("PreToolUse", "a11y-enforce-edit", { "matcher": "Edit|Write", "hooks": [{"type": "command", "command": hooks_dir + "/a11y-enforce-edit.sh"}] }) # Hook 3: PostToolUse — a11y-mark-reviewed.sh (matcher: Agent) upsert_hook("PostToolUse", "a11y-mark-reviewed", { "matcher": "Agent", "hooks": [{"type": "command", "command": hooks_dir + "/a11y-mark-reviewed.sh"}] }) with open(settings_path, "w") as f: json.dump(data, f, indent=2) PYEOF echo " + Hook 1: a11y-team-eval.sh (UserPromptSubmit — proactive web detection)" echo " + Hook 2: a11y-enforce-edit.sh (PreToolUse — blocks UI edits without review)" echo " + Hook 3: a11y-mark-reviewed.sh (PostToolUse — unlocks after review)" echo " + All 3 hooks registered in settings.json" } # --------------------------------------------------------------------------- # Installation: plugin (global) vs file-copy (project) # --------------------------------------------------------------------------- PLUGIN_INSTALL=false mkdir -p "$TARGET_DIR" MANIFEST_FILE="$TARGET_DIR/.a11y-agent-manifest" touch "$MANIFEST_FILE" add_manifest_entry() { local entry="$1" grep -qxF "$entry" "$MANIFEST_FILE" 2>/dev/null || echo "$entry" >> "$MANIFEST_FILE" } if [ "$choice" = "2" ] && [ -n "$PLUGIN_SRC" ] && command -v python3 &>/dev/null; then # Global install: register as a Claude Code plugin INSTALL_PLUGIN=true if has_tty && [ "$AUTO_APPROVE" = false ]; then echo "" printf " Would you like to install the Claude Code plugin? [Y/n]: " read -r plugin_choice < /dev/tty if [ "$plugin_choice" = "n" ] || [ "$plugin_choice" = "N" ]; then INSTALL_PLUGIN=false fi fi if [ "$INSTALL_PLUGIN" = true ]; then register_plugin "$PLUGIN_SRC" cleanup_old_install install_global_hooks PLUGIN_INSTALL=true fi else # Project install (or global without plugin support): copy agents/skills directly # Create directories mkdir -p "$TARGET_DIR/agents" if [ ${#SKILLS[@]} -gt 0 ]; then mkdir -p "$TARGET_DIR/skills" fi # Copy agents — skip any file that already exists (preserves user customisations) echo "" echo " Copying agents..." SKIPPED_AGENTS=0 for agent in "${AGENTS[@]}"; do if [ ! -f "$AGENTS_SRC/$agent" ]; then echo " ! Missing: $agent (skipped)" continue fi dst_agent="$TARGET_DIR/agents/$agent" name="${agent%.md}" if [ -f "$dst_agent" ]; then echo " ~ $name (skipped - already exists)" SKIPPED_AGENTS=$((SKIPPED_AGENTS + 1)) else cp "$AGENTS_SRC/$agent" "$dst_agent" add_manifest_entry "agents/$agent" echo " + $name" fi done if [ "$SKIPPED_AGENTS" -gt 0 ]; then echo " $SKIPPED_AGENTS agent(s) skipped. Delete them first to reinstall." fi # Copy skills — skip any file that already exists (preserves user customisations) if [ ${#SKILLS[@]} -gt 0 ]; then echo "" echo " Copying skills..." SKIPPED_SKILLS=0 for skill in "${SKILLS[@]}"; do if [ ! -f "$SKILLS_SRC/$skill" ]; then echo " ! Missing: $skill (skipped)" continue fi dst_skill="$TARGET_DIR/skills/$skill" name="${skill%.md}" if [ -f "$dst_skill" ]; then echo " ~ /$name (skipped - already exists)" SKIPPED_SKILLS=$((SKIPPED_SKILLS + 1)) else cp "$SKILLS_SRC/$skill" "$dst_skill" grep -qxF "skills/$skill" "$MANIFEST_FILE" 2>/dev/null || echo "skills/$skill" >> "$MANIFEST_FILE" echo " + /$name" fi done if [ "$SKIPPED_SKILLS" -gt 0 ]; then echo " $SKIPPED_SKILLS skill(s) skipped. Delete them first to reinstall." fi # Clean up stale commands/ directory from previous installs if [ -d "$TARGET_DIR/commands" ]; then rm -rf "$TARGET_DIR/commands" echo " ~ Removed stale commands/ directory" fi fi fi # end of project/fallback install path # Merge CLAUDE.md snippet (optional) if [ -n "$PLUGIN_CLAUDE_MD" ]; then echo "" MERGE_CLAUDE=false if read_yes_no "Merge CLAUDE.md rules?" false; then echo " Would you like to merge accessibility rules into your project CLAUDE.md?" echo " This adds the decision matrix and non-negotiable standards." MERGE_CLAUDE=true fi if [ "$MERGE_CLAUDE" = true ]; then if [ "$choice" = "1" ]; then CLAUDE_DST="$(pwd)/CLAUDE.md" else CLAUDE_DST="$HOME/CLAUDE.md" fi merge_config_file "$PLUGIN_CLAUDE_MD" "$CLAUDE_DST" "CLAUDE.md (accessibility rules)" fi fi # Copilot agents COPILOT_INSTALLED=false COPILOT_DESTINATIONS=() install_copilot=false if [ "$COPILOT_FLAG" = true ]; then install_copilot=true elif [ "$OPTIONAL_PLATFORM_FLAGS" = false ] && [ "$AUTO_APPROVE" = false ] && read_yes_no "Install Copilot agents?" false; then echo "" echo " Would you also like to install GitHub Copilot agents?" echo " This adds accessibility agents for Copilot Chat in VS Code/GitHub." install_copilot=true fi if [ "$install_copilot" = true ]; then if [ "$choice" = "1" ]; then # Project install: put agents in .github/agents/ PROJECT_DIR="$(pwd)" COPILOT_DST="$PROJECT_DIR/.github/agents" mkdir -p "$COPILOT_DST" COPILOT_DESTINATIONS+=("$COPILOT_DST") # Copy Copilot agents — skip files that already exist echo "" echo " Copying Copilot agents..." if [ -d "$COPILOT_AGENTS_SRC" ]; then for f in "$COPILOT_AGENTS_SRC"/*; do [ -f "$f" ] || continue dst_f="$COPILOT_DST/$(basename "$f")" if [ -f "$dst_f" ]; then echo " ~ $(basename "$f") (skipped - already exists)" else cp "$f" "$COPILOT_DST/" add_manifest_entry "copilot-agents/$(basename "$f")" echo " + $(basename "$f")" fi done fi # Merge Copilot config files — appends our section, never overwrites echo "" echo " Merging Copilot config..." for config in copilot-instructions.md copilot-review-instructions.md copilot-commit-message-instructions.md; do SRC="$COPILOT_CONFIG_SRC/$config" DST="$PROJECT_DIR/.github/$config" if [ -f "$SRC" ]; then merge_config_file "$SRC" "$DST" "$config" add_manifest_entry "copilot-config/$config" fi done # Copy asset subdirs — file-by-file, skip files that already exist for subdir in skills instructions prompts; do SRC_DIR="$COPILOT_CONFIG_SRC/$subdir" DST_DIR="$PROJECT_DIR/.github/$subdir" if [ -d "$SRC_DIR" ]; then # Migrate old prompt names to new agent-matching names (v2.x → v3.0) [ "$subdir" = "prompts" ] && migrate_prompts "$SRC_DIR" mkdir -p "$DST_DIR" added=0; skipped=0 while IFS= read -r -d '' src_file; do rel="${src_file#$SRC_DIR/}" dst_file="$DST_DIR/$rel" mkdir -p "$(dirname "$dst_file")" if [ -f "$dst_file" ]; then skipped=$((skipped + 1)) else cp "$src_file" "$dst_file" add_manifest_entry "copilot-$subdir/$rel" added=$((added + 1)) fi done < <(find "$SRC_DIR" -type f -print0) echo " + $subdir/ ($added new, $skipped skipped)" fi done COPILOT_INSTALLED=true else # Global install: copy .agent.md files directly into VS Code user profile folders. # This is the documented way to make agents available across all workspaces. COPILOT_CENTRAL="$HOME/.a11y-agent-team/copilot-agents" COPILOT_CENTRAL_PROMPTS="$HOME/.a11y-agent-team/copilot-prompts" COPILOT_CENTRAL_INSTRUCTIONS="$HOME/.a11y-agent-team/copilot-instructions-files" COPILOT_CENTRAL_SKILLS="$HOME/.a11y-agent-team/copilot-skills" LEGACY_COPILOT_ROOT="$HOME/.accessibility-agents" if [ ! -d "$HOME/.a11y-agent-team" ] && [ -d "$LEGACY_COPILOT_ROOT" ]; then mkdir -p "$HOME/.a11y-agent-team" cp -R "$LEGACY_COPILOT_ROOT/." "$HOME/.a11y-agent-team/" 2>/dev/null || true echo " Migrated legacy Copilot store from $LEGACY_COPILOT_ROOT to $HOME/.a11y-agent-team" fi mkdir -p "$COPILOT_CENTRAL" "$COPILOT_CENTRAL_PROMPTS" "$COPILOT_CENTRAL_INSTRUCTIONS" "$COPILOT_CENTRAL_SKILLS" # Store a central copy for updates and a11y-copilot-init echo "" echo " Storing Copilot agents centrally..." if [ -d "$COPILOT_AGENTS_SRC" ]; then for f in "$COPILOT_AGENTS_SRC"/*.agent.md; do [ -f "$f" ] || continue agent="$(basename "$f")" cp "$f" "$COPILOT_CENTRAL/$agent" name="${agent%.agent.md}" echo " + $name" done fi # Store prompts, instructions, and skills centrally # Migrate old prompt names to new agent-matching names (v2.5 → v2.6) [ -d "$COPILOT_CONFIG_SRC/prompts" ] && migrate_prompts "$COPILOT_CONFIG_SRC/prompts" [ -d "$COPILOT_CONFIG_SRC/prompts" ] && cp -r "$COPILOT_CONFIG_SRC/prompts/." "$COPILOT_CENTRAL_PROMPTS/" [ -d "$COPILOT_CONFIG_SRC/instructions" ] && cp -r "$COPILOT_CONFIG_SRC/instructions/." "$COPILOT_CENTRAL_INSTRUCTIONS/" [ -d "$COPILOT_CONFIG_SRC/skills" ] && cp -r "$COPILOT_CONFIG_SRC/skills/." "$COPILOT_CENTRAL_SKILLS/" # Copy Copilot config files to central store for config in copilot-instructions.md copilot-review-instructions.md copilot-commit-message-instructions.md; do SRC="$COPILOT_CONFIG_SRC/$config" if [ -f "$SRC" ]; then cp "$SRC" "$HOME/.a11y-agent-team/$config" fi done # Copy .agent.md files into VS Code user profile folders. # VS Code discovers agents from User/prompts/. copy_to_vscode_profile() { local profile_dir="$1" local label="$2" local prompts_dir="$profile_dir/prompts" if [ ! -d "$profile_dir" ]; then return fi mkdir -p "$prompts_dir" echo " [found] $label" # Copy agents to prompts/ for f in "$COPILOT_CENTRAL"/*.agent.md; do [ -f "$f" ] || continue cp "$f" "$prompts_dir/" done # Copy prompts and instructions to prompts/ [ -d "$COPILOT_CENTRAL_PROMPTS" ] && cp -r "$COPILOT_CENTRAL_PROMPTS/." "$prompts_dir/" [ -d "$COPILOT_CENTRAL_INSTRUCTIONS" ] && cp -r "$COPILOT_CENTRAL_INSTRUCTIONS/." "$prompts_dir/" # Clean up duplicates left by previous installer versions that wrote to User/ root for f in "$COPILOT_CENTRAL"/*.agent.md; do [ -f "$f" ] || continue rm -f "$profile_dir/$(basename "$f")" done if [ -d "$COPILOT_CENTRAL_PROMPTS" ]; then while IFS= read -r legacy_prompt; do rm -f "$profile_dir/$(basename "$legacy_prompt")" done < <(find "$COPILOT_CENTRAL_PROMPTS" -type f -name "*.prompt.md" 2>/dev/null) fi if [ -d "$COPILOT_CENTRAL_INSTRUCTIONS" ]; then while IFS= read -r legacy_instruction; do rm -f "$profile_dir/$(basename "$legacy_instruction")" done < <(find "$COPILOT_CENTRAL_INSTRUCTIONS" -type f -name "*.instructions.md" 2>/dev/null) fi echo " Copied $(ls "$COPILOT_CENTRAL"/*.agent.md 2>/dev/null | wc -l | tr -d ' ') agents" # Disable .claude/agents in VS Code so Claude Code agents # don't appear in the Copilot agent picker local settings_file="$profile_dir/settings.json" if command -v python3 &>/dev/null; then A11Y_SF="$settings_file" \ python3 - << 'PYEOF' 2>/dev/null && echo " Configured agent discovery (disabled .claude/agents)" import json, os sf = os.environ['A11Y_SF'] try: with open(sf, 'r') as f: s = json.load(f) except: s = {} loc = s.get('chat.agentFilesLocations', {}) loc['.github/agents'] = True loc['.claude/agents'] = False s['chat.agentFilesLocations'] = loc with open(sf, 'w') as f: json.dump(s, f, indent=4) PYEOF fi COPILOT_DESTINATIONS+=("$prompts_dir") } # Detect installed VS Code editions echo "" stable_selected=false insiders_selected=false while IFS='|' read -r key label path; do [ -n "$path" ] || continue [ "$key" = "stable" ] && stable_selected=true [ "$key" = "insiders" ] && insiders_selected=true done <<< "$SELECTED_COPILOT_PROFILES" if [ "$stable_selected" = true ] && [ "$insiders_selected" = true ]; then echo " Found both VS Code and VS Code Insiders." echo " Installing Copilot assets into both profiles." fi while IFS='|' read -r key label path; do [ -n "$path" ] || continue copy_to_vscode_profile "$path" "$label" done <<< "$SELECTED_COPILOT_PROFILES" if [ -z "$SELECTED_COPILOT_PROFILES" ]; then echo " No VS Code installation found. Copilot agents stored centrally only." echo " Use 'a11y-copilot-init' to copy agents into individual projects." fi # Also create a11y-copilot-init for per-project use (repos to check into git) mkdir -p "$HOME/.a11y-agent-team" INIT_SCRIPT="$HOME/.a11y-agent-team/a11y-copilot-init" cat > "$INIT_SCRIPT" << 'INITSCRIPT' #!/bin/bash # Accessibility Agents - Copy Copilot agents into the current project # Usage: a11y-copilot-init # # Copies .agent.md files into .github/agents/ for this project. # Merges copilot-instructions.md rather than overwriting it. # Skips any file that already exists to preserve your customisations. CENTRAL="$HOME/.a11y-agent-team/copilot-agents" TARGET=".github/agents" if [ ! -d "$CENTRAL" ] || [ -z "$(ls "$CENTRAL"/*.agent.md 2>/dev/null)" ]; then echo " Error: No Copilot agents found in $CENTRAL" echo " Run the accessibility-agents installer first." exit 1 fi mkdir -p "$TARGET" ADDED=0; SKIPPED=0 for f in "$CENTRAL"/*.agent.md; do [ -f "$f" ] || continue dst="$TARGET/$(basename "$f")" if [ -f "$dst" ]; then SKIPPED=$((SKIPPED+1)) else cp "$f" "$dst"; ADDED=$((ADDED+1)); fi done echo " Agents: $ADDED added, $SKIPPED skipped (already exist)" # Merge config files using accessibility-agents section markers — never overwrites user content merge_config() { local src="$1" dst="$2" label="$3" local start="" local end="" [ -f "$src" ] || return if [ ! -f "$dst" ]; then { printf '%s\n' "$start"; cat "$src"; printf '%s\n' "$end"; } > "$dst" echo " + $label (created)" return fi if grep -qF "$start" "$dst" 2>/dev/null; then if command -v python3 &>/dev/null; then python3 - "$src" "$dst" << 'PYEOF' import re, sys src_text = open(sys.argv[1]).read().rstrip() dst_path = sys.argv[2] dst_text = open(dst_path).read() start = "" end = "" block = start + "\n" + src_text + "\n" + end updated = re.sub(re.escape(start) + r".*?" + re.escape(end), block, dst_text, flags=re.DOTALL) open(dst_path, "w").write(updated) PYEOF echo " ~ $label (updated our existing section)" else echo " ! $label (section exists; python3 unavailable to update)" fi else { printf '\n%s\n' "$start"; cat "$src"; printf '%s\n' "$end"; echo; } >> "$dst" echo " + $label (merged into your existing file)" fi } for config in copilot-instructions.md copilot-review-instructions.md copilot-commit-message-instructions.md; do merge_config "$HOME/.a11y-agent-team/$config" ".github/$config" "$config" done # Copy prompts, instructions, and skills — skip existing files for pair in "copilot-prompts:prompts" "copilot-instructions-files:instructions" "copilot-skills:skills"; do SRC="$HOME/.a11y-agent-team/${pair%%:*}" DST=".github/${pair##*:}" if [ -d "$SRC" ] && [ -n "$(ls "$SRC" 2>/dev/null)" ]; then mkdir -p "$DST" added=0; skipped=0 while IFS= read -r -d '' src_file; do rel="${src_file#$SRC/}" dst_file="$DST/$rel" mkdir -p "$(dirname "$dst_file")" if [ -f "$dst_file" ]; then skipped=$((skipped+1)) else cp "$src_file" "$dst_file"; added=$((added+1)); fi done < <(find "$SRC" -type f -print0) echo " ${pair##*:}/: $added added, $skipped skipped" fi done echo "" echo " Done. Your existing files were preserved." echo " These are now in your project for version control." INITSCRIPT chmod +x "$INIT_SCRIPT" # Add to PATH if not already present SHELL_RC="" if [ -f "$HOME/.zshrc" ]; then SHELL_RC="$HOME/.zshrc" elif [ -f "$HOME/.bashrc" ]; then SHELL_RC="$HOME/.bashrc" fi if [ -n "$SHELL_RC" ]; then if ! grep -q "a11y-copilot-init" "$SHELL_RC" 2>/dev/null; then echo "" >> "$SHELL_RC" echo "# Accessibility Agents - Copilot init command" >> "$SHELL_RC" echo "export PATH=\"\$HOME/.a11y-agent-team:\$PATH\"" >> "$SHELL_RC" echo " Added 'a11y-copilot-init' command to your PATH via $SHELL_RC" else echo " 'a11y-copilot-init' already in PATH." fi fi COPILOT_INSTALLED=true add_manifest_entry "copilot-global/central-store" COPILOT_DESTINATIONS+=("$COPILOT_CENTRAL") fi fi # --------------------------------------------------------------------------- # Copilot CLI support (GitHub Copilot CLI uses ~/.copilot/) # --------------------------------------------------------------------------- COPILOT_CLI_INSTALLED=false install_copilot_cli=false if [ "$COPILOT_CLI_FLAG" = true ]; then install_copilot_cli=true elif [ "$OPTIONAL_PLATFORM_FLAGS" = false ] && [ "$AUTO_APPROVE" = false ] && read_yes_no "Install Copilot CLI agents?" false; then echo "" echo " Would you also like to install Copilot CLI agents?" echo " This adds agents to ~/.copilot/ for 'copilot' CLI use." echo " (For VS Code Copilot Chat extension, use --copilot instead)" install_copilot_cli=true fi if [ "$install_copilot_cli" = true ]; then echo "" echo " Installing Copilot CLI agents..." if [ "$choice" = "1" ]; then # Project install: put agents in .github/agents/ (CLI reads this path) PROJECT_DIR="$(pwd)" CLI_AGENTS_DST="$PROJECT_DIR/.github/agents" CLI_SKILLS_DST="$PROJECT_DIR/.github/skills" mkdir -p "$CLI_AGENTS_DST" "$CLI_SKILLS_DST" # Copy agents (skip existing) if [ -d "$COPILOT_AGENTS_SRC" ]; then for f in "$COPILOT_AGENTS_SRC"/*; do [ -f "$f" ] || continue dst_f="$CLI_AGENTS_DST/$(basename "$f")" if [ -f "$dst_f" ]; then echo " ~ $(basename "$f") (skipped - exists)" else cp "$f" "$CLI_AGENTS_DST/" echo " + $(basename "$f")" fi done fi # Copy skills (skip existing folders) SKILLS_SRC="$COPILOT_CONFIG_SRC/skills" if [ -d "$SKILLS_SRC" ]; then for skill_dir in "$SKILLS_SRC"/*/; do [ -d "$skill_dir" ] || continue skill_name=$(basename "$skill_dir") dst_skill="$CLI_SKILLS_DST/$skill_name" if [ -d "$dst_skill" ]; then echo " ~ $skill_name/ (skipped - exists)" else mkdir -p "$dst_skill" cp -R "$skill_dir"* "$dst_skill/" echo " + $skill_name/" fi done fi echo " Copilot CLI: Project agents installed to .github/agents/" else # Global install: copy to ~/.copilot/agents/ and ~/.copilot/skills/ CLI_AGENTS_DST="$HOME/.copilot/agents" CLI_SKILLS_DST="$HOME/.copilot/skills" mkdir -p "$CLI_AGENTS_DST" "$CLI_SKILLS_DST" # Copy agents if [ -d "$COPILOT_AGENTS_SRC" ]; then count=0 for f in "$COPILOT_AGENTS_SRC"/*; do [ -f "$f" ] || continue cp "$f" "$CLI_AGENTS_DST/" count=$((count + 1)) done echo " Copied $count agents to ~/.copilot/agents/" fi # Copy skills SKILLS_SRC="$COPILOT_CONFIG_SRC/skills" if [ -d "$SKILLS_SRC" ]; then count=0 for skill_dir in "$SKILLS_SRC"/*/; do [ -d "$skill_dir" ] || continue skill_name=$(basename "$skill_dir") dst_skill="$CLI_SKILLS_DST/$skill_name" mkdir -p "$dst_skill" cp -R "$skill_dir"* "$dst_skill/" count=$((count + 1)) done echo " Copied $count skills to ~/.copilot/skills/" fi echo " Copilot CLI: Global agents installed to ~/.copilot/" fi COPILOT_CLI_INSTALLED=true add_manifest_entry "copilot-cli/agents" fi # --------------------------------------------------------------------------- # Codex skills support # --------------------------------------------------------------------------- CODEX_CONFIG_SRC="$SCRIPT_DIR/.codex/config.toml" CODEX_ROLES_SRC="$SCRIPT_DIR/.codex/roles" CODEX_SKILLS_SRC="$SCRIPT_DIR/codex-skills" CODEX_INSTALLED=false install_codex=false if [ "$CODEX_FLAG" = true ]; then install_codex=true elif [ "$OPTIONAL_PLATFORM_FLAGS" = false ] && [ "$AUTO_APPROVE" = false ] && { [ -d "$CODEX_PLUGIN_SRC" ] || [ -f "$CODEX_CONFIG_SRC" ]; } && { true < /dev/tty; } 2>/dev/null; then echo "" echo " Would you also like to install Codex support?" echo " This installs the Accessibility Agents skill pack plus optional" echo " TOML-based Codex roles under .codex/config.toml and .codex/roles/." echo "" printf " Install Codex support? [y/N]: " read -r codex_choice < /dev/tty if [ "$codex_choice" = "y" ] || [ "$codex_choice" = "Y" ]; then install_codex=true fi fi if [ "$install_codex" = true ] && { [ -d "$CODEX_SKILLS_SRC" ] || [ -f "$CODEX_CONFIG_SRC" ]; }; then echo "" echo " Installing Codex support..." if [ "$choice" = "1" ]; then CODEX_TARGET_DIR="$(pwd)/.codex" mkdir -p "$CODEX_TARGET_DIR" else CODEX_TARGET_DIR="$HOME/.codex" mkdir -p "$CODEX_TARGET_DIR" fi if [ -f "$CODEX_CONFIG_SRC" ]; then CODEX_CONFIG_DST="$CODEX_TARGET_DIR/config.toml" merge_config_file "$CODEX_CONFIG_SRC" "$CODEX_CONFIG_DST" "config.toml (Codex experimental roles)" add_manifest_entry "codex-config/path:$CODEX_CONFIG_DST" fi if [ -d "$CODEX_ROLES_SRC" ]; then CODEX_ROLES_DST="$CODEX_TARGET_DIR/roles" mkdir -p "$CODEX_ROLES_DST" while IFS= read -r src_file; do rel="${src_file#$CODEX_ROLES_SRC/}" dst_file="$CODEX_ROLES_DST/$rel" mkdir -p "$(dirname "$dst_file")" cp "$src_file" "$dst_file" add_manifest_entry "codex-role/path:$dst_file" done < <(find "$CODEX_ROLES_SRC" -type f -name "*.toml" | sort) fi if [ -d "$CODEX_SKILLS_SRC" ]; then CODEX_SKILLS_DST="$CODEX_TARGET_DIR/skills" mkdir -p "$CODEX_SKILLS_DST" while IFS= read -r src_skill_dir; do rel="${src_skill_dir#$CODEX_SKILLS_SRC/}" dst_skill_dir="$CODEX_SKILLS_DST/$rel" mkdir -p "$dst_skill_dir" cp "$src_skill_dir/SKILL.md" "$dst_skill_dir/SKILL.md" add_manifest_entry "codex-skill/path:$dst_skill_dir/SKILL.md" done < <(find "$CODEX_SKILLS_SRC" -mindepth 1 -maxdepth 1 -type d | sort) echo " + Codex skills installed to $CODEX_SKILLS_DST" fi CODEX_INSTALLED=true if [ "$choice" = "1" ]; then add_manifest_entry "codex/project" else add_manifest_entry "codex/global" fi echo "" echo " Codex will now load the Accessibility Agents skills." echo " Experimental named roles are available through .codex/config.toml." echo " Codex hook support exists upstream, but it is currently experimental and" echo " only intercepts Bash/local-shell flows, not all file-edit tools." echo " Run: codex \"Review this page for accessibility issues\"." fi # --------------------------------------------------------------------------- # Gemini CLI extension # --------------------------------------------------------------------------- GEMINI_SRC="$SCRIPT_DIR/.gemini/extensions/a11y-agents" GEMINI_INSTALLED=false install_gemini=false if [ "$GEMINI_FLAG" = true ]; then install_gemini=true elif [ "$OPTIONAL_PLATFORM_FLAGS" = false ] && [ "$AUTO_APPROVE" = false ] && [ -d "$GEMINI_SRC" ] && read_yes_no "Install Gemini CLI support?" false; then echo "" echo " Would you also like to install Gemini CLI support?" echo " This installs accessibility skills as a Gemini CLI extension" echo " so Gemini automatically applies WCAG AA rules to all UI code." install_gemini=true fi if [ "$install_gemini" = true ] && [ -d "$GEMINI_SRC" ]; then echo "" echo " Installing Gemini CLI extension..." if [ "$choice" = "1" ]; then # Project install: copy to .gemini/extensions/a11y-agents/ in the current project GEMINI_TARGET="$(pwd)/.gemini/extensions/a11y-agents" else # Global install: copy to ~/.gemini/extensions/a11y-agents/ GEMINI_TARGET="$HOME/.gemini/extensions/a11y-agents" fi mkdir -p "$GEMINI_TARGET" # Copy extension manifest and context file for f in gemini-extension.json GEMINI.md; do if [ -f "$GEMINI_SRC/$f" ]; then cp "$GEMINI_SRC/$f" "$GEMINI_TARGET/$f" echo " + $f" fi done # Copy skills — directory by directory, skip existing if [ -d "$GEMINI_SRC/skills" ]; then ADDED=0; SKIPPED=0 for skill_dir in "$GEMINI_SRC/skills"/*/; do [ -d "$skill_dir" ] || continue skill_name="$(basename "$skill_dir")" dst_skill="$GEMINI_TARGET/skills/$skill_name" mkdir -p "$dst_skill" for src_file in "$skill_dir"*; do [ -f "$src_file" ] || continue dst_file="$dst_skill/$(basename "$src_file")" if [ -f "$dst_file" ]; then SKIPPED=$((SKIPPED + 1)) else cp "$src_file" "$dst_file" ADDED=$((ADDED + 1)) fi done done echo " + skills/ ($ADDED new, $SKIPPED skipped)" fi # Copy hooks — overwrite all files (hooks are versioned with the extension) if [ -d "$GEMINI_SRC/hooks" ]; then mkdir -p "$GEMINI_TARGET/hooks" ADDED=0 for src_file in "$GEMINI_SRC/hooks"/*; do [ -f "$src_file" ] || continue cp "$src_file" "$GEMINI_TARGET/hooks/$(basename "$src_file")" ADDED=$((ADDED + 1)) done echo " + hooks/ ($ADDED files)" fi GEMINI_INSTALLED=true GEMINI_DST="$GEMINI_TARGET" if [ "$choice" = "1" ]; then add_manifest_entry "gemini/project" else add_manifest_entry "gemini/global" fi add_manifest_entry "gemini/path:$GEMINI_DST" echo "" echo " Gemini CLI will now enforce WCAG AA rules on all UI code." echo " Run: gemini \"Build a login form\" — accessibility skills apply automatically." fi # --------------------------------------------------------------------------- # Guided MCP server setup # Copies the open-source MCP server to a stable location, installs npm # dependencies when available, and can configure VS Code to use it. # --------------------------------------------------------------------------- MCP_INSTALLED=false MCP_DEST="" if [ -d "$MCP_SERVER_SRC" ]; then setup_mcp=false if [ "$OPTIONAL_PLATFORM_FLAGS" = false ] && [ "$AUTO_APPROVE" = false ] && read_yes_no "Set up MCP server?" false; then echo "" echo " Would you like to set up the MCP server for document and PDF scanning?" echo " This copies the open-source server to a stable location, can install npm" echo " dependencies, and can add the VS Code MCP entry for local use." setup_mcp=true fi if [ "$setup_mcp" = true ]; then if [ "$choice" = "1" ]; then MCP_DEST="$(pwd)/mcp-server" else MCP_DEST="$HOME/.a11y-agent-team/mcp-server" fi mkdir -p "$MCP_DEST" cp -R "$MCP_SERVER_SRC/." "$MCP_DEST/" MCP_INSTALLED=true echo "" echo " MCP server copied to: $MCP_DEST" choose_mcp_capability_plan show_mcp_capability_warnings if ensure_nodejs_runtime; then install_mcp_deps=true if { true < /dev/tty; } 2>/dev/null; then echo "" echo " Node.js and npm are available." printf " Install MCP server npm dependencies now? [Y/n]: " read -r deps_choice < /dev/tty if [ "$deps_choice" = "n" ] || [ "$deps_choice" = "N" ]; then install_mcp_deps=false fi fi if [ "$install_mcp_deps" = true ]; then echo "" echo " Installing MCP server dependencies..." if (cd "$MCP_DEST" && npm install --omit=dev); then echo " + MCP server dependencies installed" else echo " ! npm install failed. You can retry later with:" echo " cd \"$MCP_DEST\" && npm install" fi fi if [ "$MCP_PLAN_PDF_FORMS" = true ]; then echo "" echo " Setting up PDF form conversion tooling..." if (cd "$MCP_DEST" && npm install pdf-lib); then echo " + pdf-lib installed" else echo " ! pdf-lib setup failed. You can retry later with:" echo " cd \"$MCP_DEST\"" echo " npm install pdf-lib" fi fi if [ "$MCP_PLAN_BROWSER" = true ]; then echo "" echo " Setting up Playwright browser tooling..." if (cd "$MCP_DEST" && npm install playwright @axe-core/playwright && npx playwright install chromium); then echo " + Playwright tooling and Chromium installed" else echo " ! Playwright setup failed. You can retry later with:" echo " cd \"$MCP_DEST\"" echo " npm install playwright @axe-core/playwright" echo " npx playwright install chromium" fi fi else echo "" echo " Node.js 18+ and npm are still not ready." echo " The MCP server was copied, but dependencies were not installed yet." echo " To enable scanning later:" echo " 1. Install Node.js 18 or later" echo " 2. Run: cd \"$MCP_DEST\" && npm install" echo " 3. Start it with: npm start" fi configure_mcp_vscode=false if [ "$MCP_PLAN_CONFIGURE_VSCODE" = true ] && { true < /dev/tty; } 2>/dev/null; then echo "" echo " Would you like to configure VS Code to use the local MCP server?" echo " This adds the HTTP endpoint http://127.0.0.1:3100/mcp to settings.json." echo "" printf " Configure VS Code MCP settings? [Y/n]: " read -r vscode_mcp_choice < /dev/tty if [ "$vscode_mcp_choice" = "" ] || [ "$vscode_mcp_choice" = "y" ] || [ "$vscode_mcp_choice" = "Y" ]; then configure_mcp_vscode=true fi fi if [ "$configure_mcp_vscode" = true ]; then if [ "$choice" = "1" ]; then configure_vscode_mcp_settings "$(pwd)/.vscode/settings.json" "http://127.0.0.1:3100/mcp" else if [ -z "$SELECTED_MCP_PROFILES" ]; then echo " ! No VS Code profile was found." echo " Add the MCP entry manually after VS Code is installed." else while IFS='|' read -r key label path; do [ -n "$path" ] || continue configure_vscode_mcp_settings "$path/settings.json" "http://127.0.0.1:3100/mcp" done <<< "$SELECTED_MCP_PROFILES" fi fi fi echo "" if command -v verapdf &>/dev/null; then echo " veraPDF detected." echo " Deep PDF/UA validation will be available through run_verapdf_scan." elif [ "$MCP_PLAN_DEEP_PDF" != true ]; then echo " Deep PDF validation was not selected during setup." echo " Baseline PDF scanning works without it." echo " If you want it later, install Java 11+ and veraPDF." echo " Windows Java via winget: winget install --exact --id EclipseAdoptium.Temurin.21.JRE" echo " Windows veraPDF via choco: choco install verapdf" echo " macOS veraPDF via Homebrew: brew install verapdf" else echo " veraPDF is not installed. That is okay." echo " Baseline PDF scanning works without it." echo " For deeper PDF/UA validation later, install Java 11+ and veraPDF:" echo " Windows Java via winget: winget install --exact --id EclipseAdoptium.Temurin.21.JRE" echo " Windows veraPDF via choco: choco install verapdf" echo " Windows veraPDF manual: https://docs.verapdf.org/install/" echo " macOS: brew install verapdf" fi fi fi # Verify installation echo "" echo " =========================" echo " Installation complete!" if [ "$PLUGIN_INSTALL" = true ]; then # Plugin-based verification CACHE_CHECK="$HOME/.claude/plugins/cache" PLUGIN_DIR="" # Find the actual cache dir (could be community-access or taylor-plugins etc) for ns_dir in "$CACHE_CHECK"/*/accessibility-agents; do [ -d "$ns_dir" ] && PLUGIN_DIR="$ns_dir/$PLUGIN_VERSION" && break done if [ -n "$PLUGIN_DIR" ] && [ -d "$PLUGIN_DIR" ]; then echo "" echo " Claude Code plugin installed:" echo "" echo " Agents:" for agent in "$PLUGIN_DIR/agents/"*.md; do [ -f "$agent" ] || continue name="$(basename "${agent%.md}")" echo " [x] $name" done echo "" echo " Skills:" for skill in "$PLUGIN_DIR/skills/"*.md; do [ -f "$skill" ] || continue name="$(basename "${skill%.md}")" echo " [x] /$name" done echo "" echo " Enforcement hooks (three-hook gate):" if [ -f "$HOME/.claude/hooks/a11y-team-eval.sh" ]; then echo " [x] UserPromptSubmit - Proactive web project detection" else echo " [ ] UserPromptSubmit - Proactive web project detection (not installed)" fi if [ -f "$HOME/.claude/hooks/a11y-enforce-edit.sh" ]; then echo " [x] PreToolUse - Blocks UI file edits until accessibility-lead reviewed" else echo " [ ] PreToolUse - Blocks UI file edits (not installed)" fi if [ -f "$HOME/.claude/hooks/a11y-mark-reviewed.sh" ]; then echo " [x] PostToolUse - Unlocks edit gate after accessibility-lead completes" else echo " [ ] PostToolUse - Unlocks edit gate (not installed)" fi fi else echo "" echo " Claude Code agents installed:" for agent in "${AGENTS[@]}"; do name="${agent%.md}" if [ -f "$TARGET_DIR/agents/$agent" ]; then echo " [x] $name" else echo " [ ] $name (missing)" fi done if [ ${#SKILLS[@]} -gt 0 ]; then echo "" echo " Skills installed:" for skill in "${SKILLS[@]}"; do name="${skill%.md}" if [ -f "$TARGET_DIR/skills/$skill" ]; then echo " [x] /$name" else echo " [ ] /$name (missing)" fi done fi fi if [ "$COPILOT_INSTALLED" = true ]; then echo "" echo " Copilot agents installed to:" for dest in "${COPILOT_DESTINATIONS[@]}"; do echo " -> $dest" done echo "" echo " Copilot agents:" for f in "${COPILOT_DESTINATIONS[0]}"/*.agent.md; do [ -f "$f" ] || continue name="$(basename "${f%.agent.md}")" echo " [x] $name" done fi if [ "$COPILOT_CLI_INSTALLED" = true ]; then echo "" echo " Copilot CLI agents installed to:" echo " -> $CLI_AGENTS_DST" echo " -> $CLI_SKILLS_DST" echo "" echo " Verify with: copilot /agent" fi if [ "$CODEX_INSTALLED" = true ]; then echo "" echo " Codex support installed to:" [ -n "$CODEX_SKILLS_DST" ] && echo " -> $CODEX_SKILLS_DST" [ -n "$CODEX_CONFIG_DST" ] && echo " -> $CODEX_CONFIG_DST" [ -n "$CODEX_ROLES_DST" ] && echo " -> $CODEX_ROLES_DST/" fi if [ "$MCP_INSTALLED" = true ]; then echo "" echo " MCP server ready at:" echo " -> $MCP_DEST" echo "" echo " Start it locally with:" echo " cd \"$MCP_DEST\" && npm start" echo "" echo " MCP endpoint: http://127.0.0.1:3100/mcp" echo " Health check: http://127.0.0.1:3100/health" show_pdf_deep_validation_readiness show_mcp_capability_readiness "$MCP_DEST" fi # Save current version hash if command -v git &>/dev/null && [ -d "$SCRIPT_DIR/.git" ]; then if [ "$PLUGIN_INSTALL" = true ]; then mkdir -p "$HOME/.claude" git -C "$SCRIPT_DIR" rev-parse --short HEAD 2>/dev/null > "$HOME/.claude/.a11y-agent-team-version" else git -C "$SCRIPT_DIR" rev-parse --short HEAD 2>/dev/null > "$TARGET_DIR/.a11y-agent-team-version" fi fi # Auto-update setup (global install only) AUTO_UPDATE_ENABLED=false if [ "$choice" = "2" ]; then echo "" if [ "$NO_AUTO_UPDATE" = true ]; then echo " Auto-updates skipped because --no-auto-update was supplied." elif [ "$AUTO_APPROVE" = true ] || read_yes_no "Enable auto-updates?" false; then echo " This checks GitHub daily for new agents and improvements." UPDATE_SCRIPT="$TARGET_DIR/.a11y-agent-team-update.sh" # Write a self-contained update script cat > "$UPDATE_SCRIPT" << 'UPDATESCRIPT' #!/bin/bash set -e REPO_URL="https://github.com/Community-Access/accessibility-agents.git" CACHE_DIR="$HOME/.claude/.a11y-agent-team-repo" INSTALL_DIR="$HOME/.claude" LOG_FILE="$HOME/.claude/.a11y-agent-team-update.log" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"; } command -v git &>/dev/null || { log "git not found"; exit 1; } if [ -d "$CACHE_DIR/.git" ]; then cd "$CACHE_DIR" || exit 1 git fetch origin main --quiet 2>/dev/null LOCAL=$(git rev-parse HEAD 2>/dev/null) REMOTE=$(git rev-parse origin/main 2>/dev/null) [ "$LOCAL" = "$REMOTE" ] && { log "Already up to date."; exit 0; } git reset --hard origin/main --quiet 2>/dev/null else mkdir -p "$(dirname "$CACHE_DIR")" git clone --quiet "$REPO_URL" "$CACHE_DIR" 2>/dev/null fi cd "$CACHE_DIR" || exit 1 HASH=$(git rev-parse --short HEAD 2>/dev/null) UPDATED=0 # Update plugin cache if installed as plugin PLUGIN_CACHE="" for ns_dir in "$HOME/.claude/plugins/cache"/*/accessibility-agents; do [ -d "$ns_dir" ] || continue for ver_dir in "$ns_dir"/*/; do [ -d "$ver_dir" ] && PLUGIN_CACHE="$ver_dir" && break done [ -n "$PLUGIN_CACHE" ] && break done if [ -n "$PLUGIN_CACHE" ] && [ -d "$CACHE_DIR/claude-code-plugin" ]; then # Update plugin cache from repo PLUGIN_SRC="$CACHE_DIR/claude-code-plugin" for subdir in agents skills scripts hooks .claude-plugin; do [ -d "$PLUGIN_SRC/$subdir" ] || continue mkdir -p "$PLUGIN_CACHE/$subdir" for SRC in "$PLUGIN_SRC/$subdir"/*; do [ -f "$SRC" ] || continue NAME=$(basename "$SRC") DST="$PLUGIN_CACHE/$subdir/$NAME" if ! cmp -s "$SRC" "$DST" 2>/dev/null; then cp "$SRC" "$DST" log "Updated plugin: $subdir/$NAME" UPDATED=$((UPDATED + 1)) fi done done # Update CLAUDE.md and README.md at plugin root for rootfile in CLAUDE.md README.md; do SRC="$PLUGIN_SRC/$rootfile" DST="$PLUGIN_CACHE/$rootfile" [ -f "$SRC" ] && ! cmp -s "$SRC" "$DST" 2>/dev/null && { cp "$SRC" "$DST" log "Updated plugin: $rootfile" UPDATED=$((UPDATED + 1)) } done chmod +x "$PLUGIN_CACHE/scripts/"*.sh 2>/dev/null || true log "Plugin cache updated." else # Legacy: update agents/commands in ~/.claude/ directly if [ -d "$CACHE_DIR/claude-code-plugin/agents" ]; then AGENT_SRC_DIR="$CACHE_DIR/claude-code-plugin/agents" else AGENT_SRC_DIR="$CACHE_DIR/.claude/agents" fi if [ -d "$INSTALL_DIR/agents" ]; then for agent in "$AGENT_SRC_DIR"/*.md; do [ -f "$agent" ] || continue NAME=$(basename "$agent") DST="$INSTALL_DIR/agents/$NAME" if ! cmp -s "$agent" "$DST" 2>/dev/null; then cp "$agent" "$DST" log "Updated: ${NAME%.md}" UPDATED=$((UPDATED + 1)) fi done fi # Update skills (check both skills/ and legacy commands/ in source) SKILL_SRC_DIR="" if [ -d "$CACHE_DIR/claude-code-plugin/skills" ]; then SKILL_SRC_DIR="$CACHE_DIR/claude-code-plugin/skills" elif [ -d "$CACHE_DIR/claude-code-plugin/commands" ]; then SKILL_SRC_DIR="$CACHE_DIR/claude-code-plugin/commands" fi # Install to skills/ dir, migrate from commands/ if needed SKILL_DST_DIR="$INSTALL_DIR/skills" [ -d "$INSTALL_DIR/commands" ] && [ ! -d "$SKILL_DST_DIR" ] && mv "$INSTALL_DIR/commands" "$SKILL_DST_DIR" if [ -n "$SKILL_SRC_DIR" ] && [ -d "$SKILL_DST_DIR" ]; then for skill in "$SKILL_SRC_DIR"/*.md; do [ -f "$skill" ] || continue NAME=$(basename "$skill") DST="$SKILL_DST_DIR/$NAME" if ! cmp -s "$skill" "$DST" 2>/dev/null; then cp "$skill" "$DST" log "Updated skill: ${NAME%.md}" UPDATED=$((UPDATED + 1)) fi done fi fi # Update Copilot agents in central store and VS Code profile folders CENTRAL="$HOME/.a11y-agent-team/copilot-agents" if [ -d "$CENTRAL" ]; then for SRC in "$CACHE_DIR"/.github/agents/*.agent.md; do [ -f "$SRC" ] || continue NAME=$(basename "$SRC") DST="$CENTRAL/$NAME" if ! cmp -s "$SRC" "$DST" 2>/dev/null; then cp "$SRC" "$DST" log "Updated Copilot agent: ${NAME%.agent.md}" UPDATED=$((UPDATED + 1)) fi done fi # Push updated Copilot agents to VS Code profile prompts folders PROFILES=() case "$(uname -s)" in Darwin) PROFILES=("$HOME/Library/Application Support/Code/User" "$HOME/Library/Application Support/Code - Insiders/User") ;; MINGW*|MSYS*|CYGWIN*) [ -n "$APPDATA" ] && PROFILES=("$APPDATA/Code/User" "$APPDATA/Code - Insiders/User") ;; esac for PROFILE in "${PROFILES[@]}"; do PROMPTS_DIR="$PROFILE/prompts" [ -d "$PROMPTS_DIR" ] && [ -n "$(ls "$PROMPTS_DIR"/*.agent.md 2>/dev/null)" ] || continue for SRC in "$CENTRAL"/*.agent.md; do [ -f "$SRC" ] || continue cp "$SRC" "$PROMPTS_DIR/" rm -f "$PROFILE/$(basename "$SRC")" done log "Updated VS Code profile: $PROFILE" done echo "$HASH" > "$INSTALL_DIR/.a11y-agent-team-version" log "Check complete: $UPDATED files updated (version $HASH)." UPDATESCRIPT chmod +x "$UPDATE_SCRIPT" # Detect platform and set up scheduler if [ "$(uname)" = "Darwin" ]; then # macOS: LaunchAgent PLIST_DIR="$HOME/Library/LaunchAgents" PLIST_FILE="$PLIST_DIR/com.community-access.a11y-agent-team-update.plist" mkdir -p "$PLIST_DIR" cat > "$PLIST_FILE" << PLIST Label com.community-access.a11y-agent-team-update ProgramArguments /bin/bash ${UPDATE_SCRIPT} StartCalendarInterval Hour 9 Minute 0 StandardOutPath ${HOME}/.claude/.a11y-agent-team-update.log StandardErrorPath ${HOME}/.claude/.a11y-agent-team-update.log RunAtLoad PLIST launchctl bootout "gui/$(id -u)" "$PLIST_FILE" 2>/dev/null || true launchctl bootstrap "gui/$(id -u)" "$PLIST_FILE" 2>/dev/null echo " Auto-updates enabled (daily at 9:00 AM via launchd)." AUTO_UPDATE_ENABLED=true else echo " Auto-update scheduling via the shell installer is supported on macOS only." echo " You can run update.sh manually anytime." fi echo " Update log: ~/.claude/.a11y-agent-team-update.log" else echo " Auto-updates skipped. You can run update.sh manually anytime." fi fi # Record install scope for uninstaller (only for file-copy installs that have a manifest) if command -v add_manifest_entry &>/dev/null 2>&1 || type add_manifest_entry &>/dev/null 2>&1; then if [ "$choice" = "1" ]; then add_manifest_entry "scope:project" else add_manifest_entry "scope:global" fi fi # Clean up temp download [ "$DOWNLOADED" = true ] && rm -rf "$TMPDIR_DL" INSTALL_NOTES=() if [ "$AUTO_APPROVE" = true ]; then INSTALL_NOTES+=("Interactive prompts were skipped with --yes.") fi if [ "$NO_AUTO_UPDATE" = true ]; then INSTALL_NOTES+=("Auto-update setup was skipped with --no-auto-update.") fi if [ "$COPILOT_INSTALLED" = true ] && [ "$VSCODE_PROFILE_MODE" != "auto" ] && [ -z "$SELECTED_COPILOT_PROFILES" ]; then INSTALL_NOTES+=("The requested VS Code profile filter did not match any installed profile for Copilot assets.") fi if [ "$MCP_INSTALLED" = true ] && [ "$MCP_PROFILE_MODE" != "auto" ] && [ -z "$SELECTED_MCP_PROFILES" ]; then INSTALL_NOTES+=("The requested MCP profile filter did not match any installed VS Code profile.") fi write_summary_file "$SUMMARY_PATH" "{\"schemaVersion\":\"1.0\",\"timestampUtc\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"operation\":\"install\",\"dryRun\":false,\"check\":false,\"scope\":\"$([ \"$choice\" = \"1\" ] && echo project || echo global)\",\"targetDir\":\"$(json_escape "$TARGET_DIR")\",\"requestedOptions\":{\"copilot\":$(json_bool "$COPILOT_FLAG"),\"copilotCli\":$(json_bool "$COPILOT_CLI_FLAG"),\"codex\":$(json_bool "$CODEX_FLAG"),\"gemini\":$(json_bool "$GEMINI_FLAG"),\"autoApprove\":$(json_bool "$AUTO_APPROVE"),\"noAutoUpdate\":$(json_bool "$NO_AUTO_UPDATE"),\"vscodeProfileMode\":\"$VSCODE_PROFILE_MODE\",\"mcpProfileMode\":\"$MCP_PROFILE_MODE\"},\"selectedCopilotProfiles\":$(json_array_from_profiles "$SELECTED_COPILOT_PROFILES" path),\"selectedMcpProfiles\":$(json_array_from_profiles "$SELECTED_MCP_PROFILES" settings),\"backupMetadataPath\":\"$(json_escape "$BACKUP_METADATA_PATH")\",\"installed\":{\"claude\":true,\"plugin\":$(json_bool "$PLUGIN_INSTALL"),\"copilot\":$(json_bool "$COPILOT_INSTALLED"),\"copilotCli\":$(json_bool "$COPILOT_CLI_INSTALLED"),\"codex\":$(json_bool "$CODEX_INSTALLED"),\"gemini\":$(json_bool "$GEMINI_INSTALLED"),\"mcp\":$(json_bool "$MCP_INSTALLED"),\"autoUpdate\":$(json_bool "$AUTO_UPDATE_ENABLED")},\"notes\":$(json_array_from_notes "${INSTALL_NOTES[@]}")}" echo "" echo " Summary written to:" echo " $SUMMARY_PATH" echo "" echo " Verification:" echo " - Re-run with --dry-run to preview profile targeting before a future change" if [ "$COPILOT_INSTALLED" = true ] && [ "$choice" = "2" ]; then echo " - Check VS Code prompts folders under the selected profiles" fi if [ "$MCP_INSTALLED" = true ]; then echo " - Start the MCP server and check http://127.0.0.1:3100/health" fi echo "" echo " Recovery:" echo " - Re-run install.sh with the same flags to repair a partial install" echo " - Use uninstall.sh if you want to remove the managed files cleanly" echo "" if [ "$PLUGIN_INSTALL" = true ]; then echo " Restart Claude Code for the plugin to take effect." echo "" echo " The plugin will:" echo " - Inject accessibility-lead delegation instruction into every UI prompt" echo " - Remind to consult accessibility-lead before editing UI files" echo " - accessibility-lead delegates to specialists via Task tool" else echo " If agents do not load, increase the character budget:" echo " export SLASH_COMMAND_TOOL_CHAR_BUDGET=30000" fi echo "" echo " To uninstall, run:" echo " curl -fsSL https://raw.githubusercontent.com/Community-Access/accessibility-agents/main/uninstall.sh | bash" echo "" echo " For manual uninstall instructions, see: UNINSTALL.md" echo "" if [ "$CODEX_INSTALLED" = true ]; then echo " Start Codex in this project and try: \"Review this component for accessibility issues\"" echo " The Accessibility Agents skills should load from .codex/skills or ~/.codex/skills." else echo " Start Claude Code and try: \"Build a login form\"" echo " The accessibility-lead should activate automatically." fi echo ""