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
180 lines
6.9 KiB
Bash
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
|
|
}
|