Last active
May 23, 2025 08:00
-
-
Save nicholaswmin/9de69f9b36ffab6e9e96ad67cbda866f to your computer and use it in GitHub Desktop.
declaratively list all installed zed.dev extensions in settings.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env zsh | |
# ext-sync.zsh | |
# | |
# Authors: 2025 - nicholaswmin - MIT | |
# | |
# Declaratively lists all installed zed.dev extensions | |
# in `settings.json` under `auto_install_extensions` | |
# - Run once before backing up dotfiles. | |
# - https://zed.dev/docs/configuring-zed#auto-install-extensions | |
# | |
# - Centralises dotfile backup. | |
# - Outputs machine/human readable format. | |
# - Autoinstalls a temporary node binary (w/o homebrew) | |
# | |
# Style: | |
# shellcheck -s bash ext-sync.zsh # no actual zsh shellcheck | |
# | |
# Usage: | |
# zsh zed-extsync.sh | |
# | |
# Environment variables: | |
# EXTENSION_DIR # Zed extension install directory | |
# SETTINGS_JSON # Zed settings file | |
# SETTINGS_KEY # Settings key for extensions | |
# NODE_VERSION # Node LTS codename (default: jod) | |
# NODE_ARCH # Node.js platform/arch (default: darwin-arm64) | |
# | |
# Flow: | |
# | |
# 1. User-configurable constants | |
# 2. Create temp dir | |
# 3. Download node LTS <lts-name> | |
# 4. Extract node | |
# 5. Setup `package.json` | |
# 6. Install node dependencies | |
# 7. Execute node script | |
# a. Read `settings.json` | |
# b. List installed extensions | |
# c. Load previous config | |
# d. Update `settings.json` extensions | |
# e. Validate new `settings.json` | |
# f. Save modified `settings.json` | |
# g. Print changes report | |
# 8. Cleanup node installations/temp dirs | |
# | |
set -e | |
DEFLT_EXT_DIR="${HOME}/Library/Application Support/Zed/extensions/installed" | |
EXTENSION_DIR="${EXTENSION_DIR:-"$DEFLT_EXT_DIR"}" | |
SETTINGS_JSON="${SETTINGS_JSON:-"$HOME/.config/zed/settings.json"}" | |
SETTINGS_KEY="${SETTINGS_KEY:-"auto_install_extensions"}" | |
NODE_VERSION="${NODE_VERSION:-"jod"}" | |
NODE_ARCH="${NODE_ARCH:-"darwin-arm64"}" | |
# show progress spinner for background jobs | |
# usage: spinner <msg> <pid> | |
spinner() { | |
local msg="$1" pid="$2" | |
if [[ ! -t 2 ]]; then | |
wait "$pid" | |
return | |
fi | |
local spin=(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏) i=1 n=${#spin[@]} | |
(( n == 0 )) && n=10 | |
# shellcheck disable=SC2059 | |
printf "%s %s" "${spin[1]}" "$msg" >&2 | |
while kill -0 "$pid" 2>/dev/null; do | |
# shellcheck disable=SC2059 | |
printf "\r%s %s" "${spin[i]}" "$msg" >&2 | |
((i=i % n + 1)) | |
sleep 0.1 | |
done | |
# shellcheck disable=SC2059 | |
printf "\r✓ %s\n" "$msg" >&2 | |
} | |
# download Node.js tarball | |
# usage: download_node <version> <outdir> | |
download_node() { | |
local version="$1" outdir="$2" | |
mkdir -p "${outdir}" | |
curl -sSL --fail -o "${outdir}/node.tar.gz" \ | |
"https://nodejs.org/dist/latest-${version}/node-latest-${version}-${NODE_ARCH}.tar.gz" | |
} | |
# extract Node.js tarball | |
# usage: extract_node <outdir> | |
extract_node() { | |
local outdir="$1" | |
mkdir -p "${outdir}/node_install" | |
tar -xzf "${outdir}/node.tar.gz" \ | |
-C "${outdir}/node_install" --strip-components=1 | |
} | |
# get node binary path | |
# usage: node_bin_path <outdir> | |
node_bin_path() { | |
local outdir="$1" | |
echo "${outdir}/node_install/bin/node" | |
} | |
# write package.json for Node.js script | |
# usage: create_package_json <dir> | |
create_package_json() { | |
local dir="$1" | |
cat > "${dir}/package.json" <<PKG | |
{ | |
"name": "zed-ext-sync", | |
"version": "1.0.0", | |
"type": "module" | |
} | |
PKG | |
} | |
# install Node.js dependencies | |
# usage: run_npm_install <dir> | |
run_npm_install() { | |
local dir="$1" | |
( | |
cd "${dir}" || exit 1 | |
npm install jsonc-parser --silent | |
) | |
} | |
# run the embedded Node.js script | |
# usage: execute_script <node_bin> <workdir> | |
execute_script() { | |
local node_bin="$1" workdir="$2" | |
( | |
cd "${workdir}" || exit 1 | |
cat > "${workdir}/zed-extsync.js" <<'EOF' | |
import fs from 'fs' | |
import { | |
parse, | |
modify, | |
printParseErrorCode as formatError | |
} from 'jsonc-parser' | |
const { SETTINGS_JSON, EXTENSION_DIR, SETTINGS_KEY } = process.env | |
// ** Utilities ** | |
const read = f => fs.readFileSync(f, 'utf8') | |
const exts = dir => | |
fs.existsSync(dir) | |
? fs.readdirSync(dir, { withFileTypes: true }) | |
.filter(path => path.isDirectory()) | |
.filter(path => !path.name.startsWith('.')) | |
.map(path => path.name) | |
: [] | |
const block = exts => Object.fromEntries(exts.map(ext => [ext, true])) | |
const edit = (text, edits) => edits.reverse().reduce( | |
(acc, edit) => | |
acc.slice(0, edit.offset) + | |
edit.content + | |
acc.slice(edit.offset + edit.length), | |
text | |
) | |
const validate = jsonc => { | |
const err = [] | |
parse(jsonc, err, { allowTrailingComma: true }) | |
if (err.length) | |
report.error(err.map(e => formatError(e.error)).join(', ')) | |
return jsonc | |
} | |
const diff = (prev, curr) => { | |
const sets = { | |
prev: new Set(Object.keys(prev || {})), | |
curr: new Set(Object.keys(curr || {})) | |
} | |
return { | |
added: [...sets.curr].filter(x => !sets.prev.has(x)), | |
removed: [...sets.prev].filter(x => !sets.curr.has(x)) | |
} | |
} | |
const old = src => parse(src)?.[SETTINGS_KEY] ?? {} | |
const report = { | |
counts({ added, removed }) { | |
console.log(` added: ${added.length}\t removed: ${removed.length} `) | |
return this | |
}, | |
br() { | |
console.log('') | |
return this | |
}, | |
hr({ added, removed }) { | |
if (!added.length && !removed.length) | |
return this | |
console.log('-----') | |
return this | |
}, | |
list({ added, removed }) { | |
if (!added.length && !removed.length) | |
return this | |
removed.forEach(ext => console.log(` \x1b[33m- ${ext}\x1b[0m `)) | |
added.forEach(ext => console.log(` \x1b[32m+ ${ext}\x1b[0m `)) | |
return this | |
}, | |
error(err) { | |
console.error(` ! \x1b[31m${err}\x1b[0m`) | |
console.error('') | |
process.exit(1) | |
} | |
} | |
// ** Main Logic ** | |
const main = () => { | |
const src = read(SETTINGS_JSON), | |
err = [], | |
opts = { | |
formattingOptions: { | |
insertSpaces: true, | |
tabSize: 2 | |
} | |
} | |
parse(src, err, { allowTrailingComma: true }) | |
if (err.length) | |
report.error(err.map(e => formatError(e.error)).join(', ')) | |
const val = block(exts(EXTENSION_DIR)) | |
const mods = modify(src, [SETTINGS_KEY], val, opts) | |
const output = mods.length ? edit(src, mods) : src | |
fs.writeFileSync(SETTINGS_JSON, validate(output), 'utf8') | |
const diffs = diff(old(src), val) | |
report.br().list(diffs).hr(diffs).counts(diffs) | |
} | |
main() | |
EOF | |
PATH="$(dirname "${node_bin}"):$PATH" \ | |
SETTINGS_JSON="${SETTINGS_JSON}" \ | |
EXTENSION_DIR="${EXTENSION_DIR}" \ | |
SETTINGS_KEY="${SETTINGS_KEY}" \ | |
"${node_bin}" "${workdir}/zed-extsync.js" | |
) | |
} | |
main() { | |
sleep 1 & | |
spinner "starting up..." $! | |
wait $! | |
local tmpdir | |
tmpdir="$(mktemp -d -t zednode.XXXXXX)" | |
trap 'rm -rf "${tmpdir}"' EXIT | |
download_node "${NODE_VERSION:-jod}" "${tmpdir}" & | |
spinner "downloading node..." $! | |
wait $! | |
extract_node "${tmpdir}" & | |
spinner "extracting node..." $! | |
wait $! | |
local node_bin | |
node_bin="$(node_bin_path "${tmpdir}")" | |
create_package_json "${tmpdir}" | |
run_npm_install "${tmpdir}" | |
execute_script "${node_bin}" "${tmpdir}" | |
} | |
main |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment