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
This commit is contained in:
max
2026-09-08 19:50:07 +02:00
co-authored by Claude Fable 5.1
parent 9085266dd3
commit c058c09050
4 changed files with 451 additions and 3 deletions
+179
View File
@@ -0,0 +1,179 @@
#!/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
}