Skip to content

Instantly share code, notes, and snippets.

@nicholaswmin
Last active May 23, 2025 08:00
Show Gist options
  • Save nicholaswmin/9de69f9b36ffab6e9e96ad67cbda866f to your computer and use it in GitHub Desktop.
Save nicholaswmin/9de69f9b36ffab6e9e96ad67cbda866f to your computer and use it in GitHub Desktop.
declaratively list all installed zed.dev extensions in settings.json
#!/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