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:
@@ -19,6 +19,10 @@ $env:GITEA_TOKEN = '<token>'
|
|||||||
# on any machine, including a fresh one
|
# on any machine, including a fresh one
|
||||||
.\scripts\install.ps1 # download from the release and install
|
.\scripts\install.ps1 # download from the release and install
|
||||||
.\scripts\settings.ps1 -Pull # apply settings, keybindings, marketplace extensions
|
.\scripts\settings.ps1 -Pull # apply settings, keybindings, marketplace extensions
|
||||||
|
|
||||||
|
# the same on Linux or macOS, no PowerShell needed
|
||||||
|
scripts/install.sh
|
||||||
|
scripts/settings.sh --pull
|
||||||
```
|
```
|
||||||
|
|
||||||
Set the server first — either in [`config.json`](config.json) or with `GITEA_URL`,
|
Set the server first — either in [`config.json`](config.json) or with `GITEA_URL`,
|
||||||
@@ -55,8 +59,10 @@ are skipped, because those are the three above and they come from the release.
|
|||||||
| [`settings.ps1`](scripts/settings.ps1) | `-Push` machine to repo, `-Pull` repo to machine, `-Diff` compares |
|
| [`settings.ps1`](scripts/settings.ps1) | `-Push` machine to repo, `-Pull` repo to machine, `-Diff` compares |
|
||||||
| [`dev-link.ps1`](scripts/dev-link.ps1) | Junctions the sources into `~/.vscode/extensions` for development. `-Unlink` undoes it |
|
| [`dev-link.ps1`](scripts/dev-link.ps1) | Junctions the sources into `~/.vscode/extensions` for development. `-Unlink` undoes it |
|
||||||
| [`ci-release.sh`](scripts/ci-release.sh) | The bash equivalent of `publish.ps1`, for the Gitea Actions runner. `SKIP_PACKAGE=1` uploads `dist/` without rebuilding |
|
| [`ci-release.sh`](scripts/ci-release.sh) | The bash equivalent of `publish.ps1`, for the Gitea Actions runner. `SKIP_PACKAGE=1` uploads `dist/` without rebuilding |
|
||||||
|
| [`install.sh`](scripts/install.sh) | The bash equivalent of `install.ps1`: `--tag`, `--local`, `--only`, `--uninstall`, `--token` |
|
||||||
|
| [`settings.sh`](scripts/settings.sh) | The bash equivalent of `settings.ps1`: `--push`, `--pull`, `--diff`, `--no-extensions` |
|
||||||
|
|
||||||
Every script takes `-?` for its full help.
|
Every PowerShell script takes `-?` for its full help, the bash ones `--help`.
|
||||||
|
|
||||||
### Tags
|
### Tags
|
||||||
|
|
||||||
@@ -176,8 +182,9 @@ once installed. Both were checked by hand.
|
|||||||
- **`dev-link.ps1` and `install.ps1` conflict.** A junctioned source and an installed
|
- **`dev-link.ps1` and `install.ps1` conflict.** A junctioned source and an installed
|
||||||
`.vsix` are two copies of the same extension id; VS Code loads one arbitrarily.
|
`.vsix` are two copies of the same extension id; VS Code loads one arbitrarily.
|
||||||
`dev-link.ps1 -Unlink` before installing.
|
`dev-link.ps1 -Unlink` before installing.
|
||||||
- **Linux and macOS need pwsh.** The scripts run under PowerShell 7 (`pwsh`) there and
|
- **Linux and macOS.** `install.sh` and `settings.sh` are bash twins of the PowerShell
|
||||||
find VS Code's user folder at `~/.config/Code/User` (`~/Library/Application Support/Code/User`
|
scripts and need only bash, curl and one of jq, node or python3 for JSON. The `.ps1`
|
||||||
|
scripts also run there under PowerShell 7 (`pwsh`). Both find VS Code's user folder at `~/.config/Code/User` (`~/Library/Application Support/Code/User`
|
||||||
on macOS) and the CLI in the usual apt, snap, flatpak and Homebrew locations. Set
|
on macOS) and the CLI in the usual apt, snap, flatpak and Homebrew locations. Set
|
||||||
`VSCODE_USER_DIR` for Insiders or a portable install. `dev-link.ps1` makes a symlink
|
`VSCODE_USER_DIR` for Insiders or a portable install. `dev-link.ps1` makes a symlink
|
||||||
instead of a junction. `ci-release.sh` needs only bash.
|
instead of a junction. `ci-release.sh` needs only bash.
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Installs the extensions into VS Code, from a Gitea release or from dist/.
|
||||||
|
#
|
||||||
|
# The bash twin of install.ps1, for a Linux or macOS machine without PowerShell. By
|
||||||
|
# default it downloads the .vsix assets of the "latest" release and installs them, so a
|
||||||
|
# fresh machine needs nothing but VS Code, curl, and this repo. --local installs what is
|
||||||
|
# already in dist/ instead.
|
||||||
|
#
|
||||||
|
# Every install passes --force: without it VS Code declines to reinstall a version it
|
||||||
|
# already has, which would silently do nothing every time the same version number is
|
||||||
|
# republished.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/install.sh install the "latest" release
|
||||||
|
# scripts/install.sh --tag v0.1.0 a specific release
|
||||||
|
# scripts/install.sh --local from dist/
|
||||||
|
# scripts/install.sh --only vertical-tabs --only colored-references
|
||||||
|
# scripts/install.sh --uninstall
|
||||||
|
# scripts/install.sh --token <token> otherwise GITEA_TOKEN or ~/.gitea-token
|
||||||
|
|
||||||
|
# shellcheck source=common.sh
|
||||||
|
. "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||||
|
|
||||||
|
tag=latest
|
||||||
|
token=""
|
||||||
|
only=""
|
||||||
|
local_install=""
|
||||||
|
uninstall=""
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--tag) tag="$2"; shift 2 ;;
|
||||||
|
--token) token="$2"; shift 2 ;;
|
||||||
|
--only) only="$only $2"; shift 2 ;;
|
||||||
|
--local) local_install=1; shift ;;
|
||||||
|
--uninstall) uninstall=1; shift ;;
|
||||||
|
-h|--help) sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||||
|
*) die "unknown argument: $1 (try --help)" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
load_config
|
||||||
|
code="$(code_command)"
|
||||||
|
extensions="$(configured_extensions "$only")"
|
||||||
|
|
||||||
|
if [ -n "$uninstall" ]; then
|
||||||
|
while IFS=$'\t' read -r id _path _repo; do
|
||||||
|
[ -n "$id" ] || continue
|
||||||
|
# The manifest's publisher is "local", so the installed id is local.<name>.
|
||||||
|
note "uninstalling local.$id"
|
||||||
|
"$code" --uninstall-extension "local.$id" || true
|
||||||
|
done <<<"$extensions"
|
||||||
|
echo "Reload the window for this to take effect."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
packages=()
|
||||||
|
staging=""
|
||||||
|
|
||||||
|
if [ -n "$local_install" ]; then
|
||||||
|
while IFS=$'\t' read -r id _path _repo; do
|
||||||
|
[ -n "$id" ] || continue
|
||||||
|
found="$(ls -1 "$setup_dist"/"$id"-*.vsix 2>/dev/null | sort -r | head -n1 || true)"
|
||||||
|
[ -n "$found" ] || die "no package for $id in $setup_dist. Run build.ps1 or ci-release.sh first."
|
||||||
|
packages+=("$found")
|
||||||
|
done <<<"$extensions"
|
||||||
|
else
|
||||||
|
require_gitea
|
||||||
|
token="$(gitea_token "$token")"
|
||||||
|
release="$(gitea_release "$tag" "$token")"
|
||||||
|
[ -n "$release" ] || die "no release tagged '$tag' in $gitea_owner/$gitea_repo. Run publish.ps1 first."
|
||||||
|
assets="$(json_rows - assets name browser_download_url <<<"$release")"
|
||||||
|
[ -n "$assets" ] || die "release '$tag' has no assets"
|
||||||
|
|
||||||
|
# Downloads go to a temp folder, not dist/, so an install never disturbs a build.
|
||||||
|
staging="$(mktemp -d -t vsix-XXXXXX)"
|
||||||
|
trap 'rm -rf "$staging"' EXIT
|
||||||
|
|
||||||
|
while IFS=$'\t' read -r id _path _repo; do
|
||||||
|
[ -n "$id" ] || continue
|
||||||
|
match="$(grep -P "^$id-.*\.vsix\t" <<<"$assets" | sort -r | head -n1 || true)"
|
||||||
|
if [ -z "$match" ]; then
|
||||||
|
warn "release '$tag' has no asset for $id, skipping"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
name="${match%%$'\t'*}"
|
||||||
|
url="${match#*$'\t'}"
|
||||||
|
note "downloading $name"
|
||||||
|
gitea_download "$url" "$staging/$name" "$token"
|
||||||
|
packages+=("$staging/$name")
|
||||||
|
done <<<"$extensions"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ ${#packages[@]} -gt 0 ] || die "nothing to install"
|
||||||
|
|
||||||
|
failed=0
|
||||||
|
for package in "${packages[@]}"; do
|
||||||
|
printf 'installing %s' "$(basename "$package")"
|
||||||
|
if "$code" --install-extension "$package" --force >/dev/null 2>&1; then
|
||||||
|
ok ' ok'
|
||||||
|
else
|
||||||
|
printf '\033[31m FAILED\033[0m\n'
|
||||||
|
failed=$((failed + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
[ "$failed" -eq 0 ] || die "$failed of ${#packages[@]} failed to install"
|
||||||
|
echo "installed ${#packages[@]} extension(s). Reload the window."
|
||||||
@@ -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."
|
||||||
Reference in New Issue
Block a user