Add portable Zed Gist sync configuration
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
#!/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 >/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 </dev/tty
|
||||
fi
|
||||
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
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user