#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" INFO_PLIST="$ROOT_DIR/Apptag/Info.plist" APPTAG_APP_SWIFT="$ROOT_DIR/Apptag/ApptagApp.swift" PREFERENCES_VIEW_SWIFT="$ROOT_DIR/Apptag/PreferencesView.swift" L10N_SWIFT="$ROOT_DIR/Apptag/L10n.swift" LOCALIZATION_DIR="$ROOT_DIR/Apptag/Localization" fail() { printf 'FAIL: %s\n' "$*" >&2 exit 1 } [[ -f "$INFO_PLIST" ]] || fail "missing Info.plist" [[ -f "$APPTAG_APP_SWIFT" ]] || fail "missing ApptagApp.swift" [[ -f "$PREFERENCES_VIEW_SWIFT" ]] || fail "missing PreferencesView.swift" [[ -f "$L10N_SWIFT" ]] || fail "missing L10n.swift" [[ -d "$LOCALIZATION_DIR" ]] || fail "missing Localization directory" plutil -lint "$INFO_PLIST" >/dev/null || fail "Info.plist is not valid" SCHEME=$(/usr/libexec/PlistBuddy -c "Print :CFBundleURLTypes:0:CFBundleURLSchemes:0" "$INFO_PLIST" 2>/dev/null || true) [[ "$SCHEME" == "taglauncher" ]] || fail "Info.plist must register taglauncher URL scheme" VERSION=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$INFO_PLIST") BUILD=$(/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$INFO_PLIST") [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail "invalid CFBundleShortVersionString: $VERSION" [[ "$BUILD" =~ ^[0-9]{8}\.[0-9]{4}$ ]] || fail "invalid CFBundleVersion: $BUILD" python3 - "$ROOT_DIR" <<'PY' import json import pathlib import re import sys root = pathlib.Path(sys.argv[1]) app = (root / "Apptag" / "ApptagApp.swift").read_text(encoding="utf-8") preferences = (root / "Apptag" / "PreferencesView.swift").read_text(encoding="utf-8") l10n = (root / "Apptag" / "L10n.swift").read_text(encoding="utf-8") localization_dir = root / "Apptag" / "Localization" def fail(message: str) -> None: print(f"FAIL: {message}", file=sys.stderr) sys.exit(1) def require(pattern: str, source: str, message: str) -> None: if not re.search(pattern, source, re.S): fail(message) require( r"func\s+application\s*\(\s*_\s+application\s*:\s*NSApplication\s*,\s*open\s+urls\s*:\s*\[URL\]\s*\)", app, "AppDelegate must implement application(_:open:) for URL scheme activation", ) require(r'externalInvocationScheme\s*=\s*"taglauncher"', app, "URL scheme constant must be taglauncher") require(r'externalInvocationShowHost\s*=\s*"show"', app, "URL show route constant must be show") require(r"pendingShowOverlayInvocation", app, "cold-launch URL requests must have a pending show flag") require(r"didFinishLaunching", app, "URL requests must be gated on launch readiness") require( r"private\s+func\s+requestShowOverlayFromExternalInvocation\s*\(\)\s*\{(?P.*?)showOrFocusOverlay\s*\(", app, "external show invocation must call showOrFocusOverlay()", ) request_match = re.search( r"private\s+func\s+requestShowOverlayFromExternalInvocation\s*\(\)\s*\{(?P.*?)\n\s*private\s+func\s+consumePendingShowOverlayInvocationIfNeeded", app, re.S, ) if not request_match: fail("could not inspect requestShowOverlayFromExternalInvocation()") request_body = request_match.group("body") if "performToggleOverlay" in request_body or "toggleOverlay" in request_body: fail("external URL show must not use toggle overlay behavior") if "dismissQuickSearchIfNeeded()" not in request_body: fail("external URL show must dismiss conflicting Quick Search transient state") require( r"private\s+func\s+consumePendingShowOverlayInvocationIfNeeded\s*\(\)\s*\{(?P.*?)requestShowOverlayFromExternalInvocation\s*\(\)", app, "launch completion must consume pending URL show requests", ) require( r"quickSearch.globalHotkey(?P.*?)trackpadGesture.title(?P.*?)taglauncher://show", preferences, "Hotkeys settings must document taglauncher://show for external gesture tools", ) for forbidden in [ "MultitouchSupport", "IOHID", "CGEventTapCreate", "AXIsProcessTrusted", "InputMonitoring", "addGlobalMonitorForEvents(matching: .gesture", ]: if forbidden in app: fail(f"AppDelegate must not add native/global trackpad capture: {forbidden}") match = re.search( r"static\s+let\s+supported\s*:\s*\[\(code:\s*String,\s*name:\s*String\)\]\s*=\s*\[(.*?)\n\s*\]", l10n, re.S, ) if not match: fail("could not read L10n.supported") codes = re.findall(r'\("([^"]+)",\s*"[^"]+"\)', match.group(1)) if len(codes) != 29: fail(f"expected 29 supported languages, found {len(codes)}") required_keys = [ "trackpadGesture.title", "trackpadGesture.description", "trackpadGesture.status", ] for code in codes: path = localization_dir / f"{code}.json" if not path.is_file(): fail(f"missing localization file for {code}") data = json.loads(path.read_text(encoding="utf-8")) missing = [key for key in required_keys if not isinstance(data.get(key), str) or not data[key].strip()] if missing: fail(f"{code}.json missing URL scheme gesture keys: {', '.join(missing)}") if "taglauncher://show" in data["trackpadGesture.description"]: fail(f"{code}.json should keep the URL in the fixed display field, not inside localized prose") print( "PASS URL scheme activation QA: taglauncher://show, show/focus semantics, " f"{len(codes)} localized settings strings, no private gesture capture" ) PY