#!/usr/bin/env bash # Searching the web from the launcher, in both the forms Panama ships it. # # There are two, deliberately: a script command that needs nothing but bash, and # an extension that adds live suggestions but has to be compiled. That is also # the risk this pins -- the search engine is written down twice, and two copies # of a URL drift. # # `vicinae script check` is the validator for the first, and it exits 0 even when # it rejects a file. Checking its exit status would pass on a script Vicinae # refuses to load, so its output is what counts. set -uo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" script="$repo_dir/config/local/share/vicinae/scripts/search-web" extension="$repo_dir/config/local/share/vicinae/extensions/panama-search" stage="$repo_dir/setup/scripts/link-vicinae-scripts" findings=() note() { findings+=("$1"); } # ── The script command ─────────────────────────────────────────────────────── if [[ ! -x "$script" ]]; then note 'search-web is missing or not executable, so Vicinae will not load it' else if command -v vicinae >/dev/null 2>&1; then output="$(vicinae script check "$script" 2>&1)" [[ "$output" == *Error* ]] && note "Vicinae rejects search-web: $output" fi # Exactly one argument. Vicinae only offers a no-view command as a fallback # -- type anything, get "Search" at the bottom -- when it takes a single # text argument, which is the whole point of shipping this one. arguments="$(grep -c '@vicinae.argument' "$script" || true)" (( arguments == 1 )) \ || note "search-web declares $arguments arguments; a fallback command takes exactly 1" grep -q '"percentEncoded": true' "$script" \ || note 'the argument is not percent-encoded, so a query containing & or # is truncated' grep -qE '@vicinae\.mode silent' "$script" \ || note 'search-web is not a silent command, so it would render a view it has nothing to put in' # The default browser is a setting this desktop already owns. Naming a # browser here would quietly outrank the Applications page. grep -q 'xdg-open' "$script" \ || note 'search-web does not open through xdg-open, so it ignores the default browser' grep -qE '\b(helium|firefox|chromium|google-chrome)\b' "$script" \ && note 'search-web names a specific browser instead of following the default' fi # ── The extension ──────────────────────────────────────────────────────────── manifest="$extension/package.json" if [[ ! -f "$manifest" ]]; then note 'the panama-search extension has no manifest' else if command -v jq >/dev/null 2>&1; then jq -e . "$manifest" >/dev/null 2>&1 || note 'the extension manifest is not valid JSON' for field in name title description commands; do jq -e "has(\"$field\")" "$manifest" >/dev/null 2>&1 \ || note "the extension manifest has no \"$field\", which Vicinae requires" done # A command's name is its source file. Getting this wrong builds an # extension with a command that cannot be opened. while read -r command_name; do [[ -n "$command_name" ]] || continue [[ -f "$extension/src/$command_name.tsx" || -f "$extension/src/$command_name.ts" ]] \ || note "the manifest declares command \"$command_name\" with no matching file in src/" done < <(jq -r '.commands[]?.name // empty' "$manifest" 2>/dev/null) fi fi # ── One engine, written twice ──────────────────────────────────────────────── # # The script command and the extension both have to know where a search goes. # Nothing makes them agree, so this does: searching from the fallback and # searching from the suggestions list must not reach different places. engine_in_script="$(grep -oE 'https://[^"]+\?q=' "$script" 2>/dev/null | head -1)" engine_in_extension="$(grep -oE 'https://[^"]+\?q=' "$extension/src/search.tsx" 2>/dev/null | head -1)" if [[ -z "$engine_in_script" ]]; then note 'no search engine URL found in search-web' elif [[ -z "$engine_in_extension" ]]; then note 'no search engine URL found in the extension' elif [[ "$engine_in_script" != "$engine_in_extension" ]]; then note "the script searches $engine_in_script but the extension searches $engine_in_extension" fi # ── Provisioning ───────────────────────────────────────────────────────────── # # An extension is compiled, so unlike a script command it cannot simply be # linked. If nothing builds it, it ships as source nobody can run. grep -q 'npm run build' "$stage" \ || note 'no stage builds the Vicinae extensions, so they never reach the launcher' grep -q 'command -v npm' "$stage" \ || note 'the extension build does not check for npm, so a machine without it fails the stage' # Checking for npm is not enough. Node comes from nvm, whose script only an # interactive shell sources -- and a stage is not one, so npm is absent unless # the stage goes and gets it. This machine hid that: a system npm existed only # because the Claude Desktop of the day pulled in nodejs. It no longer does, so # nothing hides it any more. Without this line, a fresh install builds no # extension and says so in one line nobody reads. grep -q '/etc/profile.d/nvm.sh' "$stage" \ || note 'the extension build never sources nvm, so npm is missing on any machine without a system node' # node_modules is a dependency tree, not configuration. git -C "$repo_dir" check-ignore -q "$extension/node_modules" 2>/dev/null \ || note 'the extension node_modules is not gitignored' # ── Report ─────────────────────────────────────────────────────────────────── if (( ${#findings[@]} > 0 )); then mapfile -t findings < <(printf '%s\n' "${findings[@]}" | sort -u) printf 'launcher search contract: %d finding(s)\n' "${#findings[@]}" >&2 printf ' - %s\n' "${findings[@]}" >&2 exit 1 fi printf 'launcher search contract: PASS\n'