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
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env bash
# Moves VS Code user settings between this repo and the machine.
#
# The bash twin of settings.ps1:
# --push copies the machine's settings into User/ so you can commit them
# --pull copies User/ onto the machine, backing up whatever was there
# --diff says what differs without touching anything (the default)
#
# It copies rather than symlinks on purpose: an editor that saves atomically replaces a
# symlink with a regular file and the sync silently stops. Copying is explicit.
#
# --push also records marketplace extensions in User/extensions.txt and --pull installs
# the missing ones. Ids starting "local." are skipped: those are the extensions in this
# repo, and install.sh gets them from the Gitea release. --no-extensions skips that part.
#
# Usage:
# scripts/settings.sh [--diff]
# scripts/settings.sh --push
# scripts/settings.sh --pull [--no-extensions]
# shellcheck source=common.sh
. "$(dirname "${BASH_SOURCE[0]}")/common.sh"
mode=diff
extensions=1
while [ $# -gt 0 ]; do
case "$1" in
--push) mode=push; shift ;;
--pull) mode=pull; shift ;;
--diff) mode=diff; shift ;;
--no-extensions) extensions=""; shift ;;
-h|--help) sed -n '2,19p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) die "unknown argument: $1 (try --help)" ;;
esac
done
repo_user="$setup_root/User"
code_user="$(code_user_dir)"
[ -d "$code_user" ] || die "VS Code's user folder was not found at $code_user. Set VSCODE_USER_DIR to it or copy by hand."
# Only the things worth version-controlling. Everything else in User/ is state.
items="settings.json keybindings.json tasks.json snippets"
# A hash of a file, or of every file in a directory with its relative path; empty if absent.
fingerprint() {
local path="$1"
if [ -f "$path" ]; then
sha256sum "$path" | cut -d' ' -f1
elif [ -d "$path" ]; then
(cd "$path" && find . -type f | sort | while read -r f; do
printf '%s:%s|' "$f" "$(sha256sum "$f" | cut -d' ' -f1)"
done) | sha256sum | cut -d' ' -f1
fi
}
copy_item() {
local from="$1" to="$2"
mkdir -p "$(dirname "$to")"
if [ -d "$from" ]; then
rm -rf "$to"
cp -R "$from" "$to"
else
cp "$from" "$to"
fi
}
backup_existing() {
local path="$1"
[ -e "$path" ] || return 0
local backup="$path.backup-$(date +%Y%m%d-%H%M%S)"
cp -R "$path" "$backup"
note " backed up to $(basename "$backup")"
}
echo "repo: $repo_user"
echo "code: $code_user"
echo
for item in $items; do
in_repo="$repo_user/$item"
on_disk="$code_user/$item"
repo_print="$(fingerprint "$in_repo")"
disk_print="$(fingerprint "$on_disk")"
if [ -z "$repo_print" ] && [ -z "$disk_print" ]; then
note " $item: absent both sides"; continue
fi
if [ "$repo_print" = "$disk_print" ]; then
note " $item: identical"; continue
fi
case "$mode" in
push)
if [ -z "$disk_print" ]; then warn " $item: not on this machine, leaving the repo copy alone"; continue; fi
copy_item "$on_disk" "$in_repo"
ok " $item: machine -> repo" ;;
pull)
if [ -z "$repo_print" ]; then warn " $item: not in the repo, leaving the machine alone"; continue; fi
backup_existing "$on_disk"
copy_item "$in_repo" "$on_disk"
ok " $item: repo -> machine" ;;
*)
if [ -z "$repo_print" ]; then where="only on this machine"
elif [ -z "$disk_print" ]; then where="only in the repo"
else where="differs"; fi
warn " $item: $where" ;;
esac
done
[ -n "$extensions" ] || exit 0
echo
list="$repo_user/extensions.txt"
code="$(code_command)"
if [ "$mode" = push ]; then
mkdir -p "$repo_user"
"$code" --list-extensions | grep -v '^local\.' | grep -v '^$' | sort > "$list"
ok " extensions.txt: recorded $(wc -l < "$list" | tr -d ' ') marketplace extension(s)"
exit 0
fi
if [ ! -f "$list" ]; then
warn " extensions.txt: not in the repo yet, run --push on your main machine"
exit 0
fi
present="$("$code" --list-extensions)"
missing=()
while read -r wanted; do
[ -n "$wanted" ] || continue
case "$wanted" in \#*) continue ;; esac
grep -qixF "$wanted" <<<"$present" || missing+=("$wanted")
done < "$list"
if [ "$mode" = diff ]; then
if [ ${#missing[@]} -eq 0 ]; then
note " extensions.txt: all present"
else
warn " extensions.txt: ${#missing[@]} missing: ${missing[*]}"
fi
exit 0
fi
if [ ${#missing[@]} -eq 0 ]; then
note " extensions.txt: all present"
exit 0
fi
for id in "${missing[@]}"; do
printf ' installing %s' "$id"
if "$code" --install-extension "$id" >/dev/null 2>&1; then ok ' ok'; else printf '\033[31m FAILED\033[0m\n'; fi
done
echo "Reload the window."