#!/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 } if [ -z "${ZED_SETTINGS_BUNDLE_PATH:-}" ]; then command -v gh >/dev/null 2>&1 || { printf '%s\n' 'GitHub CLI (gh) is required: https://cli.github.com/' >&2 exit 1 } unset GH_TOKEN GITHUB_TOKEN if ! gh auth status >/dev/null 2>&1; then printf '%s\n' 'GitHub authentication is required.' gh auth login /dev/tty fi fi if [ -n "${ZED_SETTINGS_GIST_ID:-}" ]; then entered=$ZED_SETTINGS_GIST_ID else printf 'GitHub Gist ID or URL (leave blank to create a new Secret Gist): ' >/dev/tty IFS= read -r entered "$response" gist_id=$(node -e 'const g=require(process.argv[1]); process.stdout.write(g.id)' "$response") gist_url=$(node -e 'const g=require(process.argv[1]); process.stdout.write(g.html_url)' "$response") printf 'Created Secret Gist: %s\n' "$gist_url" else gist_id=$(printf '%s' "$entered" | sed -nE 's#.*[^a-fA-F0-9]([a-fA-F0-9]{8,})/?$#\1#p') if [ -z "$gist_id" ]; then gist_id=$(printf '%s' "$entered" | sed -nE 's#^([a-fA-F0-9]{8,})$#\1#p') fi if [ -z "$gist_id" ]; then printf '%s\n' 'Enter a valid GitHub Gist ID or URL.' >&2 exit 1 fi gh api "gists/$gist_id" > "$response" fi if [ -z "${gist_id:-}" ]; then gist_id=$(printf '%s' "$entered" | sed -nE 's#^([a-fA-F0-9]{8,})$#\1#p') fi if [ -z "$gist_id" ]; then printf '%s\n' 'Enter a valid GitHub Gist ID or URL.' >&2 exit 1 fi node - "$response" "$gist_id" "$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