Files
vs-code-setup/scripts/common.sh
T
maxandClaude Fable 5.1 c058c09050 Bash install.sh and settings.sh for Linux and macOS
Twins of the PowerShell scripts, needing only bash, curl and one of jq,
node or python3 for JSON. install.sh downloads a release (or installs
from dist/), settings.sh pushes, pulls or diffs User/, both resolve the
code CLI and user folder per platform.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0169iPWwKHZoBTNN9qwXiwqk
2026-09-08 19:50:07 +02:00

180 lines
6.9 KiB
Bash

#!/usr/bin/env bash
# Shared pieces for install.sh and settings.sh: the bash half of common.ps1.
#
# Sourced, not run. JSON is read through whichever of jq, node or python3 is present,
# in that order -- a fresh Linux box has python3, a dev box has node, and jq is the
# nicest when it is there. Nothing here needs more than bash, curl and one of those.
set -euo pipefail
setup_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
setup_config="$setup_root/config.json"
setup_dist="$setup_root/dist"
die() { echo "error: $*" >&2; exit 1; }
note() { printf '\033[90m%s\033[0m\n' "$*"; }
ok() { printf '\033[32m%s\033[0m\n' "$*"; }
warn() { printf '\033[33m%s\033[0m\n' "$*"; }
# --- JSON ------------------------------------------------------------------------
json_tool() {
if command -v jq >/dev/null; then echo jq
elif command -v node >/dev/null; then echo node
elif command -v python3 >/dev/null; then echo python3
else die "need jq, node or python3 to read JSON"
fi
}
# json_get <file-or-'-'> <dotted.path> -> the scalar at that path, or nothing.
json_get() {
local file="$1" path="$2"
case "$(json_tool)" in
jq) jq -r "try (.$path) // empty" "$file" 2>/dev/null ;;
node) node -e '
const fs = require("fs");
const src = process.argv[1] === "-" ? 0 : process.argv[1];
let j; try { j = JSON.parse(fs.readFileSync(src, "utf8")); } catch { process.exit(0); }
const v = process.argv[2].split(".").reduce((o, k) => (o == null ? undefined : o[k]), j);
if (v !== undefined && v !== null) console.log(v);
' "$file" "$path" ;;
python3) python3 - "$file" "$path" <<'EOF'
import json, sys
src = sys.stdin if sys.argv[1] == "-" else open(sys.argv[1], encoding="utf-8")
try:
j = json.load(src)
except Exception:
sys.exit(0)
for k in sys.argv[2].split("."):
j = j.get(k) if isinstance(j, dict) else None
if j is None:
sys.exit(0)
print(j)
EOF
;;
esac
}
# json_rows <file-or-'-'> <array.path> <field1> <field2> ... -> one TSV line per element.
json_rows() {
local file="$1" path="$2"; shift 2
local fields="$*"
case "$(json_tool)" in
jq) jq -r --arg f "$fields" "try (.$path[] | [ (\$f | split(\" \"))[] as \$k | (.[\$k] // \"\" | tostring) ] | @tsv) // empty" "$file" 2>/dev/null ;;
node) node -e '
const fs = require("fs");
const src = process.argv[1] === "-" ? 0 : process.argv[1];
let j; try { j = JSON.parse(fs.readFileSync(src, "utf8")); } catch { process.exit(0); }
const arr = process.argv[2].split(".").reduce((o, k) => (o == null ? undefined : o[k]), j);
const fields = process.argv[3].split(" ");
for (const item of Array.isArray(arr) ? arr : []) console.log(fields.map(f => item[f] ?? "").join("\t"));
' "$file" "$path" "$fields" ;;
python3) python3 - "$file" "$path" "$fields" <<'EOF'
import json, sys
src = sys.stdin if sys.argv[1] == "-" else open(sys.argv[1], encoding="utf-8")
try:
j = json.load(src)
except Exception:
sys.exit(0)
for k in sys.argv[2].split("."):
j = j.get(k) if isinstance(j, dict) else None
for item in (j if isinstance(j, list) else []):
print("\t".join(str(item.get(f, "")) for f in sys.argv[3].split(" ")))
EOF
;;
esac
}
# --- config ---------------------------------------------------------------------
# GITEA_URL / GITEA_OWNER / GITEA_REPO override config.json, as in common.ps1.
load_config() {
[ -f "$setup_config" ] || die "config.json not found at $setup_config"
gitea_url="${GITEA_URL:-$(json_get "$setup_config" gitea.url)}"
gitea_owner="${GITEA_OWNER:-$(json_get "$setup_config" gitea.owner)}"
gitea_repo="${GITEA_REPO:-$(json_get "$setup_config" gitea.repo)}"
gitea_url="${gitea_url%/}"
}
require_gitea() {
case "$gitea_url" in
*example.com*|"") die "config.json still has the placeholder Gitea URL. Set it there, or set GITEA_URL, GITEA_OWNER and GITEA_REPO." ;;
esac
}
# Prints "id<TAB>path<TAB>repo" for every extension, optionally only the given ids.
configured_extensions() {
local only="${1:-}"
local rows
rows="$(json_rows "$setup_config" extensions id path repo)"
if [ -z "$only" ]; then
echo "$rows"
return
fi
local id
for id in $only; do
grep -P "^$id\t" <<<"$rows" || die "not in config.json: $id"
done
}
# --- Gitea ----------------------------------------------------------------------
# The token, from the argument, GITEA_TOKEN, or ~/.gitea-token. Never from the repo.
gitea_token() {
local token="${1:-}"
if [ -n "$token" ]; then echo "$token"; return; fi
if [ -n "${GITEA_TOKEN:-}" ]; then echo "$GITEA_TOKEN"; return; fi
if [ -s "$HOME/.gitea-token" ]; then tr -d '[:space:]' < "$HOME/.gitea-token"; return; fi
die "No Gitea token. Set GITEA_TOKEN, write one to ~/.gitea-token, or pass --token."
}
# gitea_release <tag> <token> -> the release JSON, or nothing for 404.
gitea_release() {
local tag="$1" token="$2"
local encoded
encoded="$(printf '%s' "$tag" | sed 's/ /%20/g; s/\//%2F/g')"
local status body
body="$(curl -sS -w '\n%{http_code}' -H "Authorization: token $token" \
"$gitea_url/api/v1/repos/$gitea_owner/$gitea_repo/releases/tags/$encoded")"
status="${body##*$'\n'}"
body="${body%$'\n'*}"
case "$status" in
200) echo "$body" ;;
404) ;;
*) die "Gitea answered HTTP $status for release '$tag': $body" ;;
esac
}
# gitea_download <url> <destination> <token>
#
# curl re-sends the Authorization header across redirects only to the same host, which
# is exactly the rule wanted: Gitea answers browser_download_url with a redirect to the
# attachment on the same instance, and a redirect elsewhere must not carry the token.
gitea_download() {
local url="$1" destination="$2" token="$3"
curl -fsSL --max-redirs 5 -H "Authorization: token $token" -o "$destination" "$url"
}
# --- VS Code --------------------------------------------------------------------
code_command() {
if command -v code >/dev/null; then command -v code; return; fi
local candidate
for candidate in /usr/bin/code /usr/share/code/bin/code /snap/bin/code \
/var/lib/flatpak/exports/bin/com.visualstudio.code "$HOME/.local/bin/code" \
/usr/local/bin/code "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code" \
/usr/bin/code-insiders; do
[ -x "$candidate" ] && { echo "$candidate"; return; }
done
die "The 'code' CLI was not found. In VS Code run 'Shell Command: Install code command in PATH', or add its bin/ to PATH."
}
# VS Code's user folder; VSCODE_USER_DIR overrides, as in the PowerShell scripts.
code_user_dir() {
if [ -n "${VSCODE_USER_DIR:-}" ]; then echo "$VSCODE_USER_DIR"; return; fi
case "$(uname -s)" in
Darwin) echo "$HOME/Library/Application Support/Code/User" ;;
*) echo "${XDG_CONFIG_HOME:-$HOME/.config}/Code/User" ;;
esac
}