#!/usr/bin/env sh set -eu command -v node >/dev/null 2>&1 || { printf '%s\n' 'Node.js is required. Install it with your system package manager.' >&2 exit 1 } command -v gh >/dev/null 2>&1 || { printf '%s\n' 'GitHub CLI (gh) is required: https://cli.github.com/' >&2 exit 1 } if ! gh auth status >/dev/null 2>&1; then printf '%s\n' 'GitHub authentication is required.' gh auth login /dev/tty fi if [ -n "${ZED_SETTINGS_GIST_ID:-}" ]; then entered=$ZED_SETTINGS_GIST_ID else printf 'GitHub Gist ID or full URL: ' >/dev/tty IFS= read -r entered &2 exit 1 fi temporary_dir=$(mktemp -d) trap 'rm -rf "$temporary_dir"' EXIT INT TERM response="$temporary_dir/gist.json" if [ -z "${ZED_SETTINGS_BUNDLE_PATH:-}" ]; then gh api "gists/$gist_id" > "$response" fi node - "$response" "$gist_id" "${ZED_SETTINGS_BUNDLE_PATH:-}" <<'NODE' const { createHash } = require("node:crypto"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const [responsePath, gistId, localBundlePath] = process.argv.slice(2); const bundleName = "zed-settings-sync.json"; let bundleSource; if (localBundlePath) { bundleSource = fs.readFileSync(localBundlePath, "utf8"); } else { const gist = JSON.parse(fs.readFileSync(responsePath, "utf8")); const remoteFile = gist.files?.[bundleName]; if (!remoteFile) throw new Error(`Gist ${gistId} does not contain ${bundleName}.`); if (remoteFile.truncated) throw new Error(`${bundleName} is too large for the Gist API response.`); bundleSource = remoteFile.content; } const bundle = JSON.parse(bundleSource); const fixed = new Set([ "settings.json", "keymap.json", "tasks.json", "debug.json", "scripts/toggle-file-scan-exclusions.mjs", "scripts/file-exclusions.json", "scripts/zed-settings-sync.mjs", ]); function allowed(relativePath) { const parts = relativePath.split("/"); return typeof relativePath === "string" && !relativePath.startsWith("/") && !relativePath.includes("\\") && !parts.some((part) => !part || part === "." || part === "..") && (fixed.has(relativePath) || relativePath.startsWith("snippets/") || relativePath.startsWith("themes/")); } for (const required of ["settings.json", "keymap.json", "tasks.json"]) { if (typeof bundle[required] !== "string") throw new Error(`The bundle is missing ${required}.`); } for (const [relativePath, contents] of Object.entries(bundle)) { if (!allowed(relativePath) || typeof contents !== "string") throw new Error(`Invalid bundle file: ${relativePath}`); } const target = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "zed"); const stamp = new Date().toISOString().replace(/[:.]/g, "-"); const backup = path.join(target, "backups", `gist-bootstrap-${stamp}`); fs.mkdirSync(backup, { recursive: true }); for (const relativePath of Object.keys(bundle)) { const existing = path.join(target, ...relativePath.split("/")); if (!fs.existsSync(existing)) continue; const backupPath = path.join(backup, ...relativePath.split("/")); fs.mkdirSync(path.dirname(backupPath), { recursive: true }); fs.copyFileSync(existing, backupPath); } const toggleScript = path.join(target, "scripts", "toggle-file-scan-exclusions.mjs").replaceAll("\\", "/"); const syncScript = path.join(target, "scripts", "zed-settings-sync.mjs").replaceAll("\\", "/"); for (const [relativePath, sourceContents] of Object.entries(bundle)) { const destination = path.join(target, ...relativePath.split("/")); let contents = sourceContents; if (relativePath === "tasks.json") { contents = contents.replaceAll("__TOGGLE_SCRIPT__", toggleScript).replaceAll("__SYNC_SCRIPT__", syncScript); } fs.mkdirSync(path.dirname(destination), { recursive: true }); const temporary = path.join(path.dirname(destination), `.${path.basename(destination)}.${process.pid}.tmp`); fs.writeFileSync(temporary, contents, "utf8"); fs.renameSync(temporary, destination); } const hash = createHash("sha256").update(bundleSource).digest("hex"); const syncStatePath = path.join(target, "scripts", "settings-sync.json"); fs.mkdirSync(path.dirname(syncStatePath), { recursive: true }); fs.writeFileSync(syncStatePath, `${JSON.stringify({ gist_id: gistId.toLowerCase(), last_synced_hash: hash }, null, 2)}\n`, "utf8"); console.log(`Installed Zed settings from Gist ${gistId}`); console.log(`SHA-256: ${hash}`); console.log(`Backup: ${backup}`); console.log("Restart Zed to load the synchronized configuration."); NODE