#!/usr/bin/env sh set -eu bundle_url='https://git.okk.cool/purp1e/zed-sync/raw/branch/master/zed-settings-sync.json' 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 curl >/dev/null 2>&1 || { printf '%s\n' 'curl is required. Install it with your system package manager.' >&2 exit 1 } temporary_dir=$(mktemp -d) trap 'rm -rf "$temporary_dir"' EXIT INT TERM bundle_path="$temporary_dir/zed-settings-sync.json" if [ -n "${ZED_SETTINGS_BUNDLE_PATH:-}" ]; then bundle_path=$ZED_SETTINGS_BUNDLE_PATH else printf '%s\n' 'Downloading the public Zed configuration bundle...' curl -fsSL "$bundle_url" -o "$bundle_path" fi node - "$bundle_path" <<'NODE' const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const bundlePath = process.argv[2]; const source = fs.readFileSync(bundlePath, "utf8"); const bundle = JSON.parse(source); 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", `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("/")); const contents = relativePath === "tasks.json" ? sourceContents.replaceAll("__TOGGLE_SCRIPT__", toggleScript).replaceAll("__SYNC_SCRIPT__", syncScript) : sourceContents; 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 statePath = path.join(target, "scripts", "settings-sync.json"); const previousState = fs.existsSync(statePath) ? JSON.parse(fs.readFileSync(statePath, "utf8")) : {}; const state = { gist_id: typeof previousState.gist_id === "string" ? previousState.gist_id : "", last_synced_hash: typeof previousState.last_synced_hash === "string" ? previousState.last_synced_hash : "", auto_sync: previousState.auto_sync === true, interval_minutes: Number.isInteger(previousState.interval_minutes) && previousState.interval_minutes >= 5 ? previousState.interval_minutes : 15, last_run_at: typeof previousState.last_run_at === "string" ? previousState.last_run_at : "", last_result: typeof previousState.last_result === "string" ? previousState.last_result : "", }; fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8"); console.log(`Installed Zed settings to ${target}`); console.log(`Previous settings were backed up to ${backup}`); console.log("GitHub and Gist setup are optional and did not block installation."); NODE target="${XDG_CONFIG_HOME:-$HOME/.config}/zed" if [ "${ZED_SYNC_SKIP_SETUP:-0}" != "1" ] && [ -r /dev/tty ]; then printf 'Run the GitHub sync setup now? [Y/n] ' >/dev/tty IFS= read -r answer /dev/tty; then printf '%s\n' 'Sync setup did not complete. Local installation is still valid; run the setup Task from Zed later.' >&2 fi ;; esac fi printf '%s\n' 'Restart or reload Zed to use the installed configuration.'