diff --git a/README.md b/README.md
index a3b9bf1..a384cbd 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ macOS/Linux:
curl -fsSL https://git.okk.cool/purp1e/zed-sync/raw/branch/master/bootstrap-from-gist.sh | sh
```
-脚本会处理 GitHub 登录、询问 Gist URL/ID、从 Gist 下载单个 `zed-settings-sync.json`、备份当前 Zed 配置并完成安装。安装后重启 Zed。
+脚本会处理 GitHub 登录并询问 Gist URL/ID。输入已有 Gist 会下载其配置;直接留空会从本仓库种子配置自动创建新的 Secret Gist。随后脚本会备份当前 Zed 配置并完成安装,安装后重启 Zed。
For first-machine setup, new-machine onboarding, daily Gist synchronization, and conflict handling, see [`SYNC-MANUAL.zh-CN.md`](./SYNC-MANUAL.zh-CN.md).
@@ -38,12 +38,12 @@ For first-machine setup, new-machine onboarding, daily Gist synchronization, and
Open the Task Picker and search for `Zed Settings`:
- `Export JSON...` and `Import JSON...` show a native file picker.
-- `Configure GitHub Gist...` accepts a Gist ID or complete URL and remembers it on this machine.
+- `Configure GitHub Gist...` accepts an existing Gist ID or complete URL and remembers it on this machine.
- `Show GitHub Gist Status` compares the current, remote, and last synchronized SHA-256 hashes.
-- `Push to GitHub Gist` and `Pull from GitHub Gist` use the remembered Gist and ask before overwriting divergent settings.
+- `Push to GitHub Gist` creates a new Secret Gist when none is configured; later Push/Pull operations use the remembered Gist and ask before overwriting divergent settings.
- `Authenticate GitHub...` runs `gh auth login` in the terminal.
-GitHub CLI (`gh`) must be installed. The Gist must already exist and may contain other files; synchronization only creates or replaces `zed-settings-sync.json` inside it.
+GitHub CLI (`gh`) must be installed and authenticated. An existing Gist may contain other files; synchronization only creates or replaces `zed-settings-sync.json` inside it.
The synchronized JSON is deliberately flat. Its top-level keys are paths such as `settings.json`, `keymap.json`, and `scripts/file-exclusions.json`, whose values are the complete file contents. No version, metadata, `files` wrapper, or hash is embedded. SHA-256 values are calculated at runtime and only the last synchronized value is retained locally.
diff --git a/SYNC-MANUAL.zh-CN.md b/SYNC-MANUAL.zh-CN.md
index 953c335..2e75add 100644
--- a/SYNC-MANUAL.zh-CN.md
+++ b/SYNC-MANUAL.zh-CN.md
@@ -22,9 +22,9 @@
## 2. 准备 Gist
-1. 登录 GitHub,打开 。
-2. 创建一个 Secret Gist。GitHub 不允许创建完全空白的 Gist,可以先放一个 `README.md`。
-3. 复制完整 Gist URL 或末尾的 Gist ID。
+不需要预先创建 Gist:第一次执行 Push 时,如果本机尚未配置 Gist,系统会询问是否自动创建新的 Secret Gist。
+
+已有 Gist 时,也可以登录 GitHub、打开 ,复制完整 Gist URL 或末尾的 Gist ID,再通过 `Configure GitHub Gist...` 关联。
Gist ID 不是访问令牌。身份认证由 GitHub CLI `gh` 管理。
@@ -58,13 +58,12 @@ Zed Settings: Authenticate GitHub...
gh auth login
```
-### 3.2 保存 Gist
+### 3.2 创建或关联 Gist
1. 按 `Mod+R`。
-2. 运行 `Zed Settings: Configure GitHub Gist...`。
-3. 粘贴 Gist URL 或 Gist ID。
-4. 运行 `Zed Settings: Push to GitHub Gist`。
-5. 运行 `Zed Settings: Show GitHub Gist Status`,确认状态为 `Synchronized`。
+2. 没有 Gist 时直接运行 `Zed Settings: Push to GitHub Gist`,确认自动创建。
+3. 已有 Gist 时先运行 `Zed Settings: Configure GitHub Gist...` 并粘贴 URL/ID,再执行 Push。
+4. 运行 `Zed Settings: Show GitHub Gist Status`,确认状态为 `Synchronized`。
Push 只会在该 Gist 中创建或更新 `zed-settings-sync.json`,不会删除 Gist 中的其他文件。
@@ -126,8 +125,8 @@ curl -fsSL https:///<用户>/<仓库>/raw/branch/main/bootstrap-fro
1. 检查 Node.js 和 `gh`。
2. 在尚未登录时运行 `gh auth login`。
-3. 询问 Gist URL 或 ID。
-4. 从 GitHub API 读取 `zed-settings-sync.json`。
+3. 询问 Gist URL 或 ID;留空时自动创建新的 Secret Gist。
+4. 输入已有 Gist 时从 GitHub API 读取配置;留空时使用本仓库的 `zed-settings-sync.json` 种子配置。
5. 验证顶层文件路径,防止写出 Zed 配置目录。
6. 备份新机器已有配置。
7. 安装设置、键位、Tasks、snippets、themes 和辅助脚本。
diff --git a/bootstrap-from-gist.ps1 b/bootstrap-from-gist.ps1
index 7859fda..c54b40e 100644
--- a/bootstrap-from-gist.ps1
+++ b/bootstrap-from-gist.ps1
@@ -48,18 +48,20 @@ function Write-Atomic([string]$Path, [string]$Contents) {
}
Require-Command "node" "winget install OpenJS.NodeJS.LTS"
-Require-Command "gh" "winget install GitHub.cli"
-
-& gh auth status *> $null
-if ($LASTEXITCODE -ne 0) {
- Write-Host "GitHub authentication is required."
- & gh auth login
- if ($LASTEXITCODE -ne 0) { throw "GitHub authentication failed." }
+if (-not $env:ZED_SETTINGS_BUNDLE_PATH) {
+ Require-Command "gh" "winget install GitHub.cli"
+ Remove-Item Env:GH_TOKEN,Env:GITHUB_TOKEN -ErrorAction SilentlyContinue
+ & gh auth status *> $null
+ if ($LASTEXITCODE -ne 0) {
+ Write-Host "GitHub authentication is required."
+ & gh auth login
+ if ($LASTEXITCODE -ne 0) { throw "GitHub authentication failed." }
+ }
}
-$entered = if ($env:ZED_SETTINGS_GIST_ID) { $env:ZED_SETTINGS_GIST_ID } else { Read-Host "GitHub Gist ID or full URL" }
-$gistId = Get-GistId $entered
+$entered = if ($env:ZED_SETTINGS_GIST_ID) { $env:ZED_SETTINGS_GIST_ID } else { Read-Host "GitHub Gist ID or URL (leave blank to create a new Secret Gist)" }
if ($env:ZED_SETTINGS_BUNDLE_PATH) {
+ $gistId = Get-GistId $entered
$bundleSource = [IO.File]::ReadAllText($env:ZED_SETTINGS_BUNDLE_PATH, [Text.Encoding]::UTF8)
} else {
$token = (& gh auth token).Trim()
@@ -70,11 +72,25 @@ if ($env:ZED_SETTINGS_BUNDLE_PATH) {
"User-Agent" = "zed-settings-bootstrap"
"X-GitHub-Api-Version" = "2022-11-28"
}
- $gist = Invoke-RestMethod -Uri "https://api.github.com/gists/$gistId" -Headers $headers
- $remoteFile = $gist.files.$bundleName
- if (-not $remoteFile) { throw "Gist $gistId does not contain $bundleName." }
- if ($remoteFile.truncated) { throw "$bundleName is too large for the Gist API response." }
- $bundleSource = [string]$remoteFile.content
+ if ([string]::IsNullOrWhiteSpace($entered)) {
+ $seedUrl = "https://git.okk.cool/purp1e/zed-sync/raw/branch/master/zed-settings-sync.json"
+ $bundleSource = (Invoke-WebRequest -UseBasicParsing -Uri $seedUrl).Content
+ $payload = @{
+ description = "Zed settings sync"
+ public = $false
+ files = @{ $bundleName = @{ content = $bundleSource } }
+ } | ConvertTo-Json -Depth 5
+ $created = Invoke-RestMethod -Method Post -Uri "https://api.github.com/gists" -Headers $headers -ContentType "application/json" -Body ([Text.Encoding]::UTF8.GetBytes($payload))
+ $gistId = $created.id
+ Write-Host "Created Secret Gist: $($created.html_url)"
+ } else {
+ $gistId = Get-GistId $entered
+ $gist = Invoke-RestMethod -Uri "https://api.github.com/gists/$gistId" -Headers $headers
+ $remoteFile = $gist.files.$bundleName
+ if (-not $remoteFile) { throw "Gist $gistId does not contain $bundleName." }
+ if ($remoteFile.truncated) { throw "$bundleName is too large for the Gist API response." }
+ $bundleSource = [string]$remoteFile.content
+ }
}
try {
diff --git a/bootstrap-from-gist.sh b/bootstrap-from-gist.sh
index d96953c..bfe5bd4 100644
--- a/bootstrap-from-gist.sh
+++ b/bootstrap-from-gist.sh
@@ -5,24 +5,63 @@ 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
+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 full URL: ' >/dev/tty
+ 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
@@ -30,14 +69,7 @@ if [ -z "$gist_id" ]; then
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'
+node - "$response" "$gist_id" "$bundle_path" <<'NODE'
const { createHash } = require("node:crypto");
const fs = require("node:fs");
const os = require("node:os");
diff --git a/export.ps1 b/export.ps1
index fcece97..e6bb275 100644
--- a/export.ps1
+++ b/export.ps1
@@ -22,4 +22,7 @@ foreach ($script in @(
}
[IO.File]::WriteAllText((Join-Path $target "tasks.json"), $tasks, $utf8)
+node (Join-Path $source "scripts\zed-settings-sync.mjs") export (Join-Path $target "zed-settings-sync.json")
+if ($LASTEXITCODE -ne 0) { throw "Failed to generate zed-settings-sync.json" }
+
Write-Host "Exported Zed configuration from $source"
diff --git a/export.sh b/export.sh
index 9be6e94..483dafd 100644
--- a/export.sh
+++ b/export.sh
@@ -19,4 +19,6 @@ const tasks = fs.readFileSync(source, "utf8")
fs.writeFileSync(target, tasks);
NODE
+node "$source/scripts/zed-settings-sync.mjs" export "$target/zed-settings-sync.json"
+
printf 'Exported Zed configuration from %s\n' "$source"
diff --git a/scripts/zed-settings-sync.mjs b/scripts/zed-settings-sync.mjs
index 42e6e7a..36d57ab 100644
--- a/scripts/zed-settings-sync.mjs
+++ b/scripts/zed-settings-sync.mjs
@@ -191,7 +191,7 @@ function runPowerShell(command, extraEnv = {}) {
const result = spawnSync("powershell.exe", ["-NoProfile", "-STA", "-Command", command], {
encoding: "utf8",
env: { ...process.env, ...extraEnv },
- windowsHide: true,
+ windowsHide: false,
});
if (result.status !== 0) throw new Error(result.stderr.trim() || "Windows dialog failed");
return result.stdout.trim();
@@ -199,10 +199,10 @@ function runPowerShell(command, extraEnv = {}) {
function nativeFileDialog(mode) {
if (process.platform === "win32") {
- const common = "[Console]::OutputEncoding=[Text.Encoding]::UTF8; Add-Type -AssemblyName System.Windows.Forms; ";
+ const common = "[Console]::OutputEncoding=[Text.Encoding]::UTF8; Add-Type -AssemblyName System.Windows.Forms; $o=New-Object System.Windows.Forms.Form; $o.TopMost=$true; $o.ShowInTaskbar=$false; $o.Opacity=0; $o.Show(); $o.Activate(); ";
const command = mode === "save"
- ? `${common}$d=New-Object System.Windows.Forms.SaveFileDialog; $d.Filter='Zed settings (*.json)|*.json'; $d.FileName='${BUNDLE_NAME}'; $d.AddExtension=$true; if($d.ShowDialog() -eq 'OK'){[Console]::Write($d.FileName)}`
- : `${common}$d=New-Object System.Windows.Forms.OpenFileDialog; $d.Filter='Zed settings (*.json)|*.json'; $d.FileName='${BUNDLE_NAME}'; if($d.ShowDialog() -eq 'OK'){[Console]::Write($d.FileName)}`;
+ ? `${common}$d=New-Object System.Windows.Forms.SaveFileDialog; $d.Filter='Zed settings (*.json)|*.json'; $d.FileName='${BUNDLE_NAME}'; $d.AddExtension=$true; if($d.ShowDialog($o) -eq 'OK'){[Console]::Write($d.FileName)}; $o.Close()`
+ : `${common}$d=New-Object System.Windows.Forms.OpenFileDialog; $d.Filter='Zed settings (*.json)|*.json'; $d.FileName='${BUNDLE_NAME}'; if($d.ShowDialog($o) -eq 'OK'){[Console]::Write($d.FileName)}; $o.Close()`;
return runPowerShell(command);
}
if (process.platform === "darwin") {
@@ -220,7 +220,42 @@ function nativeFileDialog(mode) {
function nativeInput(prompt, title, defaultValue = "") {
if (process.platform === "win32") {
- const command = "[Console]::OutputEncoding=[Text.Encoding]::UTF8; Add-Type -AssemblyName Microsoft.VisualBasic; [Console]::Write([Microsoft.VisualBasic.Interaction]::InputBox($env:ZED_SYNC_PROMPT,$env:ZED_SYNC_TITLE,$env:ZED_SYNC_DEFAULT))";
+ const command = `
+ [Console]::OutputEncoding=[Text.Encoding]::UTF8
+ Add-Type -AssemblyName System.Windows.Forms
+ Add-Type -AssemblyName System.Drawing
+ $f=New-Object System.Windows.Forms.Form
+ $f.Text=$env:ZED_SYNC_TITLE
+ $f.Size=New-Object System.Drawing.Size(620,190)
+ $f.StartPosition='CenterScreen'
+ $f.TopMost=$true
+ $f.ShowInTaskbar=$true
+ $f.FormBorderStyle='FixedDialog'
+ $f.MaximizeBox=$false
+ $f.MinimizeBox=$false
+ $l=New-Object System.Windows.Forms.Label
+ $l.Text=$env:ZED_SYNC_PROMPT
+ $l.AutoSize=$false
+ $l.Location=New-Object System.Drawing.Point(14,14)
+ $l.Size=New-Object System.Drawing.Size(575,42)
+ $t=New-Object System.Windows.Forms.TextBox
+ $t.Text=$env:ZED_SYNC_DEFAULT
+ $t.Location=New-Object System.Drawing.Point(14,62)
+ $t.Size=New-Object System.Drawing.Size(575,24)
+ $ok=New-Object System.Windows.Forms.Button
+ $ok.Text='OK'
+ $ok.DialogResult=[System.Windows.Forms.DialogResult]::OK
+ $ok.Location=New-Object System.Drawing.Point(427,105)
+ $cancel=New-Object System.Windows.Forms.Button
+ $cancel.Text='Cancel'
+ $cancel.DialogResult=[System.Windows.Forms.DialogResult]::Cancel
+ $cancel.Location=New-Object System.Drawing.Point(514,105)
+ $f.Controls.AddRange(@($l,$t,$ok,$cancel))
+ $f.AcceptButton=$ok
+ $f.CancelButton=$cancel
+ $f.Add_Shown({$f.Activate();$t.Focus();$t.SelectAll()})
+ if($f.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK){[Console]::Write($t.Text)}
+ `;
return runPowerShell(command, {
ZED_SYNC_PROMPT: prompt,
ZED_SYNC_TITLE: title,
@@ -239,7 +274,7 @@ function nativeInput(prompt, title, defaultValue = "") {
function nativeMessage(message, title = "Zed Settings Sync", error = false) {
if (process.platform === "win32") {
- const command = "Add-Type -AssemblyName System.Windows.Forms; [void][System.Windows.Forms.MessageBox]::Show($env:ZED_SYNC_MESSAGE,$env:ZED_SYNC_TITLE,'OK',$env:ZED_SYNC_ICON)";
+ const command = "Add-Type -AssemblyName System.Windows.Forms; $o=New-Object System.Windows.Forms.Form; $o.TopMost=$true; $o.ShowInTaskbar=$false; $o.Opacity=0; $o.Show(); $o.Activate(); [void][System.Windows.Forms.MessageBox]::Show($o,$env:ZED_SYNC_MESSAGE,$env:ZED_SYNC_TITLE,'OK',$env:ZED_SYNC_ICON); $o.Close()";
runPowerShell(command, {
ZED_SYNC_MESSAGE: message,
ZED_SYNC_TITLE: title,
@@ -257,7 +292,7 @@ function nativeMessage(message, title = "Zed Settings Sync", error = false) {
function nativeConfirm(message, title = "Zed Settings Sync") {
if (process.platform === "win32") {
- const command = "Add-Type -AssemblyName System.Windows.Forms; [Console]::Write([System.Windows.Forms.MessageBox]::Show($env:ZED_SYNC_MESSAGE,$env:ZED_SYNC_TITLE,'YesNo','Warning'))";
+ const command = "Add-Type -AssemblyName System.Windows.Forms; $o=New-Object System.Windows.Forms.Form; $o.TopMost=$true; $o.ShowInTaskbar=$false; $o.Opacity=0; $o.Show(); $o.Activate(); [Console]::Write([System.Windows.Forms.MessageBox]::Show($o,$env:ZED_SYNC_MESSAGE,$env:ZED_SYNC_TITLE,'YesNo','Warning')); $o.Close()";
return runPowerShell(command, { ZED_SYNC_MESSAGE: message, ZED_SYNC_TITLE: title }) === "Yes";
}
if (process.platform === "darwin") {
@@ -297,6 +332,7 @@ function normalizeGistId(value) {
async function promptForGist() {
const state = await readSyncConfiguration();
+ console.log("Opening Gist configuration dialog...");
const entered = nativeInput(
"Paste the GitHub Gist ID or full Gist URL. The previous value is remembered on this machine.",
"Zed Settings Gist",
@@ -313,7 +349,10 @@ async function promptForGist() {
}
function runGh(args, input) {
- const result = spawnSync("gh", args, { encoding: "utf8", input, windowsHide: true });
+ const env = { ...process.env };
+ delete env.GH_TOKEN;
+ delete env.GITHUB_TOKEN;
+ const result = spawnSync("gh", args, { encoding: "utf8", env, input, windowsHide: true });
if (result.error?.code === "ENOENT") {
throw new Error("GitHub CLI (gh) is not installed. Install it, then run: gh auth login");
}
@@ -334,6 +373,15 @@ function updateRemoteGist(gistId, contents) {
return JSON.parse(runGh(["api", "--method", "PATCH", `gists/${gistId}`, "--input", "-"], payload));
}
+function createRemoteGist(contents) {
+ const payload = JSON.stringify({
+ description: "Zed settings sync",
+ public: false,
+ files: { [BUNDLE_NAME]: { content: contents } },
+ });
+ return JSON.parse(runGh(["api", "--method", "POST", "gists", "--input", "-"], payload));
+}
+
function shortHash(hash) {
return hash ? hash.slice(0, 12) : "none";
}
@@ -345,9 +393,21 @@ async function configureGist() {
}
async function pushGist() {
- const state = await promptForGist();
- if (!state) return;
+ const state = await readSyncConfiguration();
const localBundle = await createBundle();
+ if (!state.config.gist_id) {
+ console.log("No Gist is configured. Asking to create one...");
+ const proceed = nativeConfirm(
+ "No GitHub Gist is configured. Create a new Secret Gist from this machine's current Zed settings?",
+ );
+ if (!proceed) return;
+ const created = createRemoteGist(localBundle.contents);
+ state.config.gist_id = created.id;
+ state.config.last_synced_hash = localBundle.hash;
+ await saveSyncConfiguration(state.path, state.config);
+ nativeMessage(`Created and uploaded Secret Gist:\n${created.html_url || created.id}\n\nSHA-256: ${localBundle.hash}`);
+ return;
+ }
const remote = readRemoteGist(state.config.gist_id);
const remoteHash = remote.parsed?.hash || "";
const lastHash = state.config.last_synced_hash;
diff --git a/tasks.json b/tasks.json
index fd8e95f..5f6c988 100644
--- a/tasks.json
+++ b/tasks.json
@@ -107,8 +107,8 @@
"gist-configure"
],
"allow_concurrent_runs": false,
- "reveal": "never",
- "hide": "always",
+ "reveal": "no_focus",
+ "hide": "never",
"show_summary": false,
"show_command": false,
"save": "all"
@@ -121,8 +121,8 @@
"gist-status"
],
"allow_concurrent_runs": false,
- "reveal": "never",
- "hide": "always",
+ "reveal": "no_focus",
+ "hide": "never",
"show_summary": false,
"show_command": false,
"save": "all"
@@ -136,7 +136,7 @@
],
"allow_concurrent_runs": false,
"reveal": "no_focus",
- "hide": "on_success",
+ "hide": "never",
"show_summary": false,
"show_command": false,
"save": "all"
@@ -150,15 +150,19 @@
],
"allow_concurrent_runs": false,
"reveal": "no_focus",
- "hide": "on_success",
+ "hide": "never",
"show_summary": false,
"show_command": false,
"save": "all"
},
{
"label": "Zed Settings: Authenticate GitHub...",
- "command": "gh",
- "args": ["auth", "login"],
+ "command": "powershell",
+ "args": [
+ "-NoExit",
+ "-Command",
+ "Remove-Item Env:GH_TOKEN,Env:GITHUB_TOKEN -ErrorAction SilentlyContinue; gh auth login"
+ ],
"allow_concurrent_runs": false,
"reveal": "always",
"hide": "never",
diff --git a/zed-settings-sync.json b/zed-settings-sync.json
new file mode 100644
index 0000000..ae8c777
--- /dev/null
+++ b/zed-settings-sync.json
@@ -0,0 +1,8 @@
+{
+ "keymap.json": "[\n {\n \"bindings\": {\n \"f12\": [\"zed::IncreaseUiFontSize\", { \"persist\": false }],\n \"secondary-f12\": [\"zed::DecreaseUiFontSize\", { \"persist\": false }],\n \"secondary-1\": \"project_panel::ToggleFocus\",\n \"secondary-2\": \"terminal_panel::ToggleFocus\",\n \"secondary-3\": \"workspace::ToggleRightDock\",\n \"secondary-shift-g\": \"git_panel::ToggleFocus\",\n \"secondary-shift-v\": \"zed::Extensions\",\n \"secondary-p\": \"command_palette::Toggle\",\n \"secondary-o\": \"file_finder::Toggle\",\n \"secondary-9\": \"pane::ActivatePreviousItem\",\n \"secondary-0\": \"pane::ActivateNextItem\",\n \"alt-1\": [\"workspace::ActivatePane\", 0],\n \"alt-2\": [\"workspace::ActivatePane\", 1],\n \"alt-3\": [\"workspace::ActivatePane\", 2],\n \"alt-4\": [\"workspace::ActivatePane\", 3],\n \"alt-5\": [\"workspace::ActivatePane\", 4],\n \"secondary-shift-o\": \"workspace::Open\",\n \"secondary-enter\": \"workspace::ToggleZoom\",\n \"secondary-r\": \"task::Spawn\",\n \"secondary-shift-r\": \"projects::OpenRecent\",\n \"shift-alt-r\": null,\n \"secondary-; k\": \"workspace::Reload\",\n \"secondary-; g\": \"git::FileHistory\",\n \"secondary-; p\": \"git::Push\",\n \"secondary-; secondary-p\": \"git::Pull\",\n \"secondary-; n\": \"git::Branch\",\n \"secondary-; s\": \"git::StashAll\",\n \"secondary-; z\": \"git::StashPop\",\n \"secondary-; a\": \"git::StageAll\",\n \"secondary-; u\": [\n \"task::Spawn\",\n { \"task_name\": \"Zed Settings: Push to GitHub Gist\" }\n ],\n \"secondary-; d\": [\n \"task::Spawn\",\n { \"task_name\": \"Zed Settings: Pull from GitHub Gist\" }\n ],\n \"shift-alt--\": [\n \"task::Spawn\",\n { \"task_name\": \"Toggle excluded files\" }\n ]\n }\n },\n {\n \"context\": \"Workspace\",\n \"bindings\": {\n \"secondary-; c\": \"git_panel::ToggleFocus\"\n }\n },\n {\n \"context\": \"GitPanel\",\n \"bindings\": {\n \"secondary-; c\": \"git_panel::FocusEditor\"\n }\n },\n {\n \"context\": \"Editor && mode == full\",\n \"bindings\": {\n \"secondary-shift-[\": \"editor::Fold\",\n \"secondary-shift-]\": \"editor::UnfoldLines\",\n \"secondary--\": \"editor::FoldAll\",\n \"secondary-=\": \"editor::UnfoldAll\"\n }\n },\n {\n \"context\": \"Editor && extension == md\",\n \"bindings\": {\n \"alt-p\": \"markdown::OpenPreview\",\n \"shift-alt-p\": \"markdown::OpenPreviewToTheSide\"\n }\n },\n {\n \"context\": \"ProjectPanel\",\n \"bindings\": {\n \"alt--\": \"project_panel::CollapseAllEntries\"\n }\n }\n]\n",
+ "scripts/file-exclusions.json": "{\n \"defaults\": [\n \"**/.git\",\n \"**/.svn\",\n \"**/.hg\",\n \"**/.jj\",\n \"**/CVS\",\n \"**/.DS_Store\",\n \"**/Thumbs.db\",\n \"**/.classpath\",\n \"**/.settings\"\n ],\n \"custom\": [\n \"**/.playwright-cli\",\n \"**/output\",\n \"**/.cursor\",\n \"**/.vscode\",\n \"**/.idea\",\n \"**/.fleet\",\n \"**/node_modules\",\n \"**/bun.lockb\",\n \"**/pnpm-lock.yaml\",\n \"**/package-lock.json\",\n \"**/yarn.lock\",\n \"**/dist\",\n \"**/out\",\n \"**/build\",\n \"**/.next\",\n \"**/.nuxt\",\n \"**/.output\",\n \"**/.cache\",\n \"**/.turbo\",\n \"**/.vercel\",\n \"**/.netlify\",\n \"**/tsconfig.tsbuildinfo\",\n \"**/auto-imports.d.ts\",\n \"**/components.json\",\n \"**/unplugin-vue-components.d.ts\",\n \"src/*.d.ts\",\n \"**/__pycache__\",\n \"**/__init__.py\",\n \"**/.ipynb_checkpoints\",\n \"**/.pytest_cache\",\n \"**/.mypy_cache\",\n \"**/.venv\",\n \"**/env\",\n \"**/venv\",\n \"**/.github\",\n \"**/.husky\",\n \"**/.npmrc\",\n \"**/.eslintrc*\",\n \"**/.prettier*\",\n \"**/.editorconfig\",\n \"**/.dockerignore\",\n \"**/docker-compose.override.yml\",\n \"**/cypress*\",\n \"**/prisma/migrations\"\n ]\n}\n",
+ "scripts/toggle-file-scan-exclusions.mjs": "import { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { readFile, rename, rm, writeFile } from \"node:fs/promises\";\nimport { fileURLToPath } from \"node:url\";\n\nconst scriptDirectory = dirname(fileURLToPath(import.meta.url));\n\nasync function exclusionRules() {\n const rulesPath = join(scriptDirectory, \"file-exclusions.json\");\n const rules = JSON.parse(await readFile(rulesPath, \"utf8\"));\n if (!Array.isArray(rules.defaults) || !Array.isArray(rules.custom)) {\n throw new Error(\"file-exclusions.json must contain defaults and custom arrays\");\n }\n return {\n defaults: [...new Set(rules.defaults)],\n custom: [...new Set(rules.custom)].filter(\n (entry) => !rules.defaults.includes(entry),\n ),\n };\n}\n\nfunction settingsPath() {\n if (process.platform === \"win32\") {\n const appData = process.env.APPDATA;\n if (!appData) throw new Error(\"APPDATA is not set\");\n return join(appData, \"Zed\", \"settings.json\");\n }\n return join(homedir(), \".config\", \"zed\", \"settings.json\");\n}\n\nfunction findArrayRange(source) {\n const property = /\"file_scan_exclusions\"\\s*:/g.exec(source);\n if (!property) throw new Error(\"file_scan_exclusions is missing from settings.json\");\n\n const start = source.indexOf(\"[\", property.index + property[0].length);\n if (start === -1) throw new Error(\"file_scan_exclusions is not an array\");\n\n let inString = false;\n let escaped = false;\n let depth = 0;\n for (let index = start; index < source.length; index += 1) {\n const character = source[index];\n if (inString) {\n if (escaped) escaped = false;\n else if (character === \"\\\\\") escaped = true;\n else if (character === '\"') inString = false;\n continue;\n }\n if (character === '\"') inString = true;\n else if (character === \"[\") depth += 1;\n else if (character === \"]\" && --depth === 0) return { start, end: index + 1 };\n }\n throw new Error(\"file_scan_exclusions array is not closed\");\n}\n\nfunction formatArray(values, indent, newline) {\n const itemIndent = `${indent} `;\n const items = values.map((value) => `${itemIndent}${JSON.stringify(value)}`);\n return `[${newline}${items.join(`,${newline}`)}${newline}${indent}]`;\n}\n\nasync function replaceAtomically(path, contents) {\n const temporaryPath = join(dirname(path), `.settings.json.${process.pid}.tmp`);\n try {\n await writeFile(temporaryPath, contents, \"utf8\");\n await rename(temporaryPath, path);\n } finally {\n await rm(temporaryPath, { force: true });\n }\n}\n\nconst path = settingsPath();\nconst { defaults, custom } = await exclusionRules();\nconst source = await readFile(path, \"utf8\");\nconst newline = source.includes(\"\\r\\n\") ? \"\\r\\n\" : \"\\n\";\nconst range = findArrayRange(source);\nconst current = JSON.parse(source.slice(range.start, range.end));\nconst customIsActive = current.some((entry) => !defaults.includes(entry));\nconst next = customIsActive ? defaults : [...defaults, ...custom];\nconst lineStart = source.lastIndexOf(\"\\n\", range.start) + 1;\nconst indent = source.slice(lineStart, range.start).match(/^\\s*/)?.[0] ?? \"\";\nconst updated =\n source.slice(0, range.start) +\n formatArray(next, indent, newline) +\n source.slice(range.end);\n\nawait replaceAtomically(path, updated);\nconsole.log(`Excluded files are now ${customIsActive ? \"shown\" : \"hidden\"}.`);\n",
+ "scripts/zed-settings-sync.mjs": "import { createHash } from \"node:crypto\";\nimport { copyFile, mkdir, readFile, readdir, rename, rm, writeFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { basename, dirname, join, resolve } from \"node:path\";\nimport { spawnSync } from \"node:child_process\";\n\nconst BUNDLE_NAME = \"zed-settings-sync.json\";\nconst REQUIRED_FILES = [\"settings.json\", \"keymap.json\", \"tasks.json\"];\nconst FIXED_FILES = [\n ...REQUIRED_FILES,\n \"debug.json\",\n \"scripts/toggle-file-scan-exclusions.mjs\",\n \"scripts/file-exclusions.json\",\n \"scripts/zed-settings-sync.mjs\",\n];\nconst OPTIONAL_DIRECTORIES = [\"snippets\", \"themes\"];\nconst LOCAL_SYNC_CONFIG = \"scripts/settings-sync.json\";\n\nfunction zedConfigDirectory() {\n if (process.platform === \"win32\") {\n if (!process.env.APPDATA) throw new Error(\"APPDATA is not set\");\n return join(process.env.APPDATA, \"Zed\");\n }\n return join(process.env.XDG_CONFIG_HOME || join(homedir(), \".config\"), \"zed\");\n}\n\nfunction timestamp() {\n return new Date().toISOString().replace(/[:.]/g, \"-\");\n}\n\nasync function writeAtomically(path, contents) {\n await mkdir(dirname(path), { recursive: true });\n const temporaryPath = join(dirname(path), `.${basename(path)}.${process.pid}.tmp`);\n try {\n await writeFile(temporaryPath, contents, \"utf8\");\n await rename(temporaryPath, path);\n } finally {\n await rm(temporaryPath, { force: true });\n }\n}\n\nasync function listFilesRecursively(directory, prefix) {\n if (!existsSync(directory)) return [];\n const results = [];\n for (const entry of await readdir(directory, { withFileTypes: true })) {\n const absolutePath = join(directory, entry.name);\n const relativePath = `${prefix}/${entry.name}`;\n if (entry.isDirectory()) results.push(...await listFilesRecursively(absolutePath, relativePath));\n else if (entry.isFile()) results.push(relativePath);\n }\n return results;\n}\n\nasync function synchronizedPaths(configDirectory) {\n const paths = FIXED_FILES.filter((path) => existsSync(join(configDirectory, ...path.split(\"/\"))));\n for (const directory of OPTIONAL_DIRECTORIES) {\n paths.push(...await listFilesRecursively(join(configDirectory, directory), directory));\n }\n return [...new Set(paths)].sort();\n}\n\nfunction isAllowedBundlePath(path) {\n if (typeof path !== \"string\" || path.startsWith(\"/\") || path.includes(\"\\\\\")) return false;\n const segments = path.split(\"/\");\n if (segments.some((segment) => !segment || segment === \".\" || segment === \"..\")) return false;\n if (FIXED_FILES.includes(path) || path === LOCAL_SYNC_CONFIG) return true;\n return OPTIONAL_DIRECTORIES.some((directory) => path.startsWith(`${directory}/`));\n}\n\nfunction replaceTaskPath(source, path, placeholder) {\n return source\n .replaceAll(path.replaceAll(\"\\\\\", \"\\\\\\\\\"), placeholder)\n .replaceAll(path.replaceAll(\"\\\\\", \"/\"), placeholder);\n}\n\nfunction portableTasks(source, configDirectory) {\n return replaceTaskPath(\n replaceTaskPath(\n source,\n join(configDirectory, \"scripts\", \"toggle-file-scan-exclusions.mjs\"),\n \"__TOGGLE_SCRIPT__\",\n ),\n join(configDirectory, \"scripts\", \"zed-settings-sync.mjs\"),\n \"__SYNC_SCRIPT__\",\n );\n}\n\nfunction installedTasks(source, configDirectory) {\n return source\n .replaceAll(\n \"__TOGGLE_SCRIPT__\",\n join(configDirectory, \"scripts\", \"toggle-file-scan-exclusions.mjs\").replaceAll(\"\\\\\", \"/\"),\n )\n .replaceAll(\n \"__SYNC_SCRIPT__\",\n join(configDirectory, \"scripts\", \"zed-settings-sync.mjs\").replaceAll(\"\\\\\", \"/\"),\n );\n}\n\nfunction hashContents(contents) {\n return createHash(\"sha256\").update(contents).digest(\"hex\");\n}\n\nasync function createBundle() {\n const configDirectory = zedConfigDirectory();\n const files = {};\n for (const relativePath of await synchronizedPaths(configDirectory)) {\n const absolutePath = join(configDirectory, ...relativePath.split(\"/\"));\n let contents = await readFile(absolutePath, \"utf8\");\n if (relativePath === \"tasks.json\") contents = portableTasks(contents, configDirectory);\n files[relativePath] = contents;\n }\n for (const required of REQUIRED_FILES) {\n if (!(required in files)) throw new Error(`Cannot export: ${required} is missing`);\n }\n const contents = serializeBundle(files);\n return { files, contents, hash: hashContents(contents) };\n}\n\nfunction serializeBundle(files) {\n return `${JSON.stringify(files, null, 2)}\\n`;\n}\n\nfunction parseBundle(contents, sourceName) {\n let files;\n try {\n files = JSON.parse(contents);\n } catch (error) {\n throw new Error(`Invalid JSON in ${sourceName}: ${error.message}`);\n }\n if (!files || Array.isArray(files) || typeof files !== \"object\") {\n throw new Error(`Invalid Zed settings bundle: ${sourceName}`);\n }\n for (const required of REQUIRED_FILES) {\n if (typeof files[required] !== \"string\") {\n throw new Error(`Invalid bundle: ${required} is missing`);\n }\n }\n for (const [path, value] of Object.entries(files)) {\n if (!isAllowedBundlePath(path) || typeof value !== \"string\") {\n throw new Error(`Invalid bundle file: ${path}`);\n }\n }\n return { files, hash: hashContents(contents) };\n}\n\nasync function exportBundle(path) {\n const bundle = await createBundle();\n await writeAtomically(path, bundle.contents);\n console.log(`Exported Zed settings to ${path}`);\n console.log(`SHA-256: ${bundle.hash}`);\n return bundle;\n}\n\nasync function backupCurrentConfiguration(configDirectory) {\n const backupDirectory = join(configDirectory, \"backups\", `sync-import-${timestamp()}`);\n await mkdir(backupDirectory, { recursive: true });\n for (const relativePath of await synchronizedPaths(configDirectory)) {\n const source = join(configDirectory, ...relativePath.split(\"/\"));\n const destination = join(backupDirectory, ...relativePath.split(\"/\"));\n await mkdir(dirname(destination), { recursive: true });\n await copyFile(source, destination);\n }\n return backupDirectory;\n}\n\nasync function installBundleContents(contents, sourceName) {\n const parsed = parseBundle(contents, sourceName);\n const configDirectory = zedConfigDirectory();\n const backupDirectory = await backupCurrentConfiguration(configDirectory);\n for (const [relativePath, sourceContents] of Object.entries(parsed.files)) {\n // Gist identity and sync history belong to each machine and are never imported.\n if (relativePath === LOCAL_SYNC_CONFIG) continue;\n const destination = join(configDirectory, ...relativePath.split(\"/\"));\n const installedContents = relativePath === \"tasks.json\"\n ? installedTasks(sourceContents, configDirectory)\n : sourceContents;\n await writeAtomically(destination, installedContents);\n }\n console.log(`Imported Zed settings from ${sourceName}`);\n console.log(`Previous settings were backed up to ${backupDirectory}`);\n return { ...parsed, backupDirectory };\n}\n\nasync function importBundle(path) {\n return installBundleContents(await readFile(path, \"utf8\"), path);\n}\n\nfunction runPowerShell(command, extraEnv = {}) {\n const result = spawnSync(\"powershell.exe\", [\"-NoProfile\", \"-STA\", \"-Command\", command], {\n encoding: \"utf8\",\n env: { ...process.env, ...extraEnv },\n windowsHide: false,\n });\n if (result.status !== 0) throw new Error(result.stderr.trim() || \"Windows dialog failed\");\n return result.stdout.trim();\n}\n\nfunction nativeFileDialog(mode) {\n if (process.platform === \"win32\") {\n const common = \"[Console]::OutputEncoding=[Text.Encoding]::UTF8; Add-Type -AssemblyName System.Windows.Forms; $o=New-Object System.Windows.Forms.Form; $o.TopMost=$true; $o.ShowInTaskbar=$false; $o.Opacity=0; $o.Show(); $o.Activate(); \";\n const command = mode === \"save\"\n ? `${common}$d=New-Object System.Windows.Forms.SaveFileDialog; $d.Filter='Zed settings (*.json)|*.json'; $d.FileName='${BUNDLE_NAME}'; $d.AddExtension=$true; if($d.ShowDialog($o) -eq 'OK'){[Console]::Write($d.FileName)}; $o.Close()`\n : `${common}$d=New-Object System.Windows.Forms.OpenFileDialog; $d.Filter='Zed settings (*.json)|*.json'; $d.FileName='${BUNDLE_NAME}'; if($d.ShowDialog($o) -eq 'OK'){[Console]::Write($d.FileName)}; $o.Close()`;\n return runPowerShell(command);\n }\n if (process.platform === \"darwin\") {\n const script = mode === \"save\"\n ? `POSIX path of (choose file name with prompt \"Export Zed settings\" default name \"${BUNDLE_NAME}\")`\n : \"POSIX path of (choose file with prompt \\\"Import Zed settings\\\")\";\n const result = spawnSync(\"osascript\", [\"-e\", script], { encoding: \"utf8\" });\n return result.status === 0 ? result.stdout.trim() : \"\";\n }\n const args = [\"--file-selection\", `--title=${mode === \"save\" ? \"Export\" : \"Import\"} Zed settings`];\n if (mode === \"save\") args.push(\"--save\", \"--confirm-overwrite\", `--filename=${BUNDLE_NAME}`);\n const result = spawnSync(\"zenity\", args, { encoding: \"utf8\" });\n return result.status === 0 ? result.stdout.trim() : \"\";\n}\n\nfunction nativeInput(prompt, title, defaultValue = \"\") {\n if (process.platform === \"win32\") {\n const command = `\n [Console]::OutputEncoding=[Text.Encoding]::UTF8\n Add-Type -AssemblyName System.Windows.Forms\n Add-Type -AssemblyName System.Drawing\n $f=New-Object System.Windows.Forms.Form\n $f.Text=$env:ZED_SYNC_TITLE\n $f.Size=New-Object System.Drawing.Size(620,190)\n $f.StartPosition='CenterScreen'\n $f.TopMost=$true\n $f.ShowInTaskbar=$true\n $f.FormBorderStyle='FixedDialog'\n $f.MaximizeBox=$false\n $f.MinimizeBox=$false\n $l=New-Object System.Windows.Forms.Label\n $l.Text=$env:ZED_SYNC_PROMPT\n $l.AutoSize=$false\n $l.Location=New-Object System.Drawing.Point(14,14)\n $l.Size=New-Object System.Drawing.Size(575,42)\n $t=New-Object System.Windows.Forms.TextBox\n $t.Text=$env:ZED_SYNC_DEFAULT\n $t.Location=New-Object System.Drawing.Point(14,62)\n $t.Size=New-Object System.Drawing.Size(575,24)\n $ok=New-Object System.Windows.Forms.Button\n $ok.Text='OK'\n $ok.DialogResult=[System.Windows.Forms.DialogResult]::OK\n $ok.Location=New-Object System.Drawing.Point(427,105)\n $cancel=New-Object System.Windows.Forms.Button\n $cancel.Text='Cancel'\n $cancel.DialogResult=[System.Windows.Forms.DialogResult]::Cancel\n $cancel.Location=New-Object System.Drawing.Point(514,105)\n $f.Controls.AddRange(@($l,$t,$ok,$cancel))\n $f.AcceptButton=$ok\n $f.CancelButton=$cancel\n $f.Add_Shown({$f.Activate();$t.Focus();$t.SelectAll()})\n if($f.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK){[Console]::Write($t.Text)}\n `;\n return runPowerShell(command, {\n ZED_SYNC_PROMPT: prompt,\n ZED_SYNC_TITLE: title,\n ZED_SYNC_DEFAULT: defaultValue,\n });\n }\n if (process.platform === \"darwin\") {\n const escaped = (value) => value.replaceAll(\"\\\\\", \"\\\\\\\\\").replaceAll('\"', '\\\\\"');\n const script = `text returned of (display dialog \"${escaped(prompt)}\" with title \"${escaped(title)}\" default answer \"${escaped(defaultValue)}\")`;\n const result = spawnSync(\"osascript\", [\"-e\", script], { encoding: \"utf8\" });\n return result.status === 0 ? result.stdout.trim() : \"\";\n }\n const result = spawnSync(\"zenity\", [\"--entry\", `--title=${title}`, `--text=${prompt}`, `--entry-text=${defaultValue}`], { encoding: \"utf8\" });\n return result.status === 0 ? result.stdout.trim() : \"\";\n}\n\nfunction nativeMessage(message, title = \"Zed Settings Sync\", error = false) {\n if (process.platform === \"win32\") {\n const command = \"Add-Type -AssemblyName System.Windows.Forms; $o=New-Object System.Windows.Forms.Form; $o.TopMost=$true; $o.ShowInTaskbar=$false; $o.Opacity=0; $o.Show(); $o.Activate(); [void][System.Windows.Forms.MessageBox]::Show($o,$env:ZED_SYNC_MESSAGE,$env:ZED_SYNC_TITLE,'OK',$env:ZED_SYNC_ICON); $o.Close()\";\n runPowerShell(command, {\n ZED_SYNC_MESSAGE: message,\n ZED_SYNC_TITLE: title,\n ZED_SYNC_ICON: error ? \"Error\" : \"Information\",\n });\n return;\n }\n if (process.platform === \"darwin\") {\n const escaped = message.replaceAll(\"\\\\\", \"\\\\\\\\\").replaceAll('\"', '\\\\\"');\n spawnSync(\"osascript\", [\"-e\", `display dialog \"${escaped}\" with title \"${title}\" buttons {\"OK\"}`]);\n return;\n }\n spawnSync(\"zenity\", [error ? \"--error\" : \"--info\", `--title=${title}`, `--text=${message}`]);\n}\n\nfunction nativeConfirm(message, title = \"Zed Settings Sync\") {\n if (process.platform === \"win32\") {\n const command = \"Add-Type -AssemblyName System.Windows.Forms; $o=New-Object System.Windows.Forms.Form; $o.TopMost=$true; $o.ShowInTaskbar=$false; $o.Opacity=0; $o.Show(); $o.Activate(); [Console]::Write([System.Windows.Forms.MessageBox]::Show($o,$env:ZED_SYNC_MESSAGE,$env:ZED_SYNC_TITLE,'YesNo','Warning')); $o.Close()\";\n return runPowerShell(command, { ZED_SYNC_MESSAGE: message, ZED_SYNC_TITLE: title }) === \"Yes\";\n }\n if (process.platform === \"darwin\") {\n const escaped = message.replaceAll(\"\\\\\", \"\\\\\\\\\").replaceAll('\"', '\\\\\"');\n const result = spawnSync(\"osascript\", [\"-e\", `button returned of (display dialog \"${escaped}\" with title \"${title}\" buttons {\"Cancel\", \"Continue\"} default button \"Continue\" cancel button \"Cancel\")`], { encoding: \"utf8\" });\n return result.status === 0 && result.stdout.trim() === \"Continue\";\n }\n return spawnSync(\"zenity\", [\"--question\", `--title=${title}`, `--text=${message}`]).status === 0;\n}\n\nasync function readSyncConfiguration() {\n const path = join(zedConfigDirectory(), ...LOCAL_SYNC_CONFIG.split(\"/\"));\n let source = {};\n try {\n source = JSON.parse(await readFile(path, \"utf8\"));\n } catch (error) {\n if (error.code !== \"ENOENT\") throw error;\n }\n return {\n path,\n config: {\n gist_id: typeof source.gist_id === \"string\" ? source.gist_id : \"\",\n last_synced_hash: typeof source.last_synced_hash === \"string\" ? source.last_synced_hash : \"\",\n },\n };\n}\n\nasync function saveSyncConfiguration(path, config) {\n await writeAtomically(path, `${JSON.stringify(config, null, 2)}\\n`);\n}\n\nfunction normalizeGistId(value) {\n const matches = value.trim().match(/[a-f0-9]{8,}/gi);\n if (!matches?.length) throw new Error(\"Enter a valid GitHub Gist ID or URL\");\n return matches.at(-1).toLowerCase();\n}\n\nasync function promptForGist() {\n const state = await readSyncConfiguration();\n console.log(\"Opening Gist configuration dialog...\");\n const entered = nativeInput(\n \"Paste the GitHub Gist ID or full Gist URL. The previous value is remembered on this machine.\",\n \"Zed Settings Gist\",\n state.config.gist_id,\n );\n if (!entered) return null;\n const gistId = normalizeGistId(entered);\n if (gistId !== state.config.gist_id) {\n state.config.gist_id = gistId;\n state.config.last_synced_hash = \"\";\n await saveSyncConfiguration(state.path, state.config);\n }\n return state;\n}\n\nfunction runGh(args, input) {\n const env = { ...process.env };\n delete env.GH_TOKEN;\n delete env.GITHUB_TOKEN;\n const result = spawnSync(\"gh\", args, { encoding: \"utf8\", env, input, windowsHide: true });\n if (result.error?.code === \"ENOENT\") {\n throw new Error(\"GitHub CLI (gh) is not installed. Install it, then run: gh auth login\");\n }\n if (result.status !== 0) throw new Error(result.stderr.trim() || `gh ${args[0]} failed`);\n return result.stdout.trim();\n}\n\nfunction readRemoteGist(gistId) {\n const gist = JSON.parse(runGh([\"api\", `gists/${gistId}`]));\n const file = gist.files?.[BUNDLE_NAME];\n if (!file) return { gist, parsed: null, contents: null };\n if (file.truncated) throw new Error(`${BUNDLE_NAME} is too large to read through the Gist API`);\n return { gist, parsed: parseBundle(file.content, `Gist ${gistId}`), contents: file.content };\n}\n\nfunction updateRemoteGist(gistId, contents) {\n const payload = JSON.stringify({ files: { [BUNDLE_NAME]: { content: contents } } });\n return JSON.parse(runGh([\"api\", \"--method\", \"PATCH\", `gists/${gistId}`, \"--input\", \"-\"], payload));\n}\n\nfunction createRemoteGist(contents) {\n const payload = JSON.stringify({\n description: \"Zed settings sync\",\n public: false,\n files: { [BUNDLE_NAME]: { content: contents } },\n });\n return JSON.parse(runGh([\"api\", \"--method\", \"POST\", \"gists\", \"--input\", \"-\"], payload));\n}\n\nfunction shortHash(hash) {\n return hash ? hash.slice(0, 12) : \"none\";\n}\n\nasync function configureGist() {\n const state = await promptForGist();\n if (!state) return;\n nativeMessage(`Saved Gist ID:\\n${state.config.gist_id}\\n\\nAuthentication is managed by GitHub CLI (gh).`);\n}\n\nasync function pushGist() {\n const state = await readSyncConfiguration();\n const localBundle = await createBundle();\n if (!state.config.gist_id) {\n console.log(\"No Gist is configured. Asking to create one...\");\n const proceed = nativeConfirm(\n \"No GitHub Gist is configured. Create a new Secret Gist from this machine's current Zed settings?\",\n );\n if (!proceed) return;\n const created = createRemoteGist(localBundle.contents);\n state.config.gist_id = created.id;\n state.config.last_synced_hash = localBundle.hash;\n await saveSyncConfiguration(state.path, state.config);\n nativeMessage(`Created and uploaded Secret Gist:\\n${created.html_url || created.id}\\n\\nSHA-256: ${localBundle.hash}`);\n return;\n }\n const remote = readRemoteGist(state.config.gist_id);\n const remoteHash = remote.parsed?.hash || \"\";\n const lastHash = state.config.last_synced_hash;\n if (remoteHash && remoteHash !== localBundle.hash) {\n const remoteChanged = !lastHash || remoteHash !== lastHash;\n if (remoteChanged) {\n const proceed = nativeConfirm(\n `The Gist contains different settings.\\n\\nLocal: ${shortHash(localBundle.hash)}\\nRemote: ${shortHash(remoteHash)}\\nLast sync: ${shortHash(lastHash)}\\n\\nOverwrite the Gist with this machine's settings?`,\n );\n if (!proceed) return;\n }\n }\n const updated = updateRemoteGist(state.config.gist_id, localBundle.contents);\n state.config.last_synced_hash = localBundle.hash;\n await saveSyncConfiguration(state.path, state.config);\n nativeMessage(`Uploaded to Gist ${state.config.gist_id}\\n\\nSHA-256: ${localBundle.hash}\\nUpdated: ${updated.updated_at || \"unknown\"}`);\n}\n\nasync function pullGist() {\n const state = await promptForGist();\n if (!state) return;\n const localBundle = await createBundle();\n const remote = readRemoteGist(state.config.gist_id);\n if (!remote.parsed) throw new Error(`Gist ${state.config.gist_id} does not contain ${BUNDLE_NAME}`);\n if (localBundle.hash !== remote.parsed.hash) {\n const localChanged = !state.config.last_synced_hash || localBundle.hash !== state.config.last_synced_hash;\n if (localChanged) {\n const proceed = nativeConfirm(\n `This machine contains different settings.\\n\\nLocal: ${shortHash(localBundle.hash)}\\nRemote: ${shortHash(remote.parsed.hash)}\\nLast sync: ${shortHash(state.config.last_synced_hash)}\\n\\nReplace local settings with the Gist version? A backup will be created first.`,\n );\n if (!proceed) return;\n }\n await installBundleContents(remote.contents, `Gist ${state.config.gist_id}`);\n }\n state.config.last_synced_hash = remote.parsed.hash;\n await saveSyncConfiguration(state.path, state.config);\n nativeMessage(`Downloaded from Gist ${state.config.gist_id}\\n\\nSHA-256: ${remote.parsed.hash}\\nRemote updated: ${remote.gist.updated_at || \"unknown\"}`);\n}\n\nasync function showGistStatus() {\n const state = await promptForGist();\n if (!state) return;\n const localBundle = await createBundle();\n const remote = readRemoteGist(state.config.gist_id);\n const remoteHash = remote.parsed?.hash || \"\";\n const lastHash = state.config.last_synced_hash;\n let status = \"The Gist does not contain a Zed settings file.\";\n if (remoteHash === localBundle.hash) status = \"Synchronized\";\n else if (!lastHash) status = \"Not synchronized on this machine\";\n else if (localBundle.hash === lastHash) status = \"Remote changes available\";\n else if (remoteHash === lastHash) status = \"Local changes pending upload\";\n else status = \"Conflict: local and remote settings both changed\";\n nativeMessage(\n `Gist: ${remote.gist.html_url || state.config.gist_id}\\nStatus: ${status}\\n\\nLocal: ${shortHash(localBundle.hash)}\\nRemote: ${shortHash(remoteHash)}\\nLast sync: ${shortHash(lastHash)}\\nRemote updated: ${remote.gist.updated_at || \"unknown\"}`,\n \"Zed Settings Gist Status\",\n );\n}\n\nasync function main() {\n const [mode, explicitPath] = process.argv.slice(2);\n if (mode === \"export\") return exportBundle(resolve(explicitPath));\n if (mode === \"import\") return importBundle(resolve(explicitPath));\n if (mode === \"export-dialog\") {\n const path = nativeFileDialog(\"save\");\n if (path) await exportBundle(path);\n return;\n }\n if (mode === \"import-dialog\") {\n const path = nativeFileDialog(\"open\");\n if (path) await importBundle(path);\n return;\n }\n if (mode === \"gist-configure\") return configureGist();\n if (mode === \"gist-status\") return showGistStatus();\n if (mode === \"gist-push\") return pushGist();\n if (mode === \"gist-pull\") return pullGist();\n throw new Error(`Unknown mode: ${mode || \"\"}`);\n}\n\ntry {\n await main();\n} catch (error) {\n console.error(error.stack || error.message);\n try {\n nativeMessage(error.message, \"Zed Settings Sync Error\", true);\n } catch {\n // The terminal output remains available when the native dialog backend is unavailable.\n }\n process.exitCode = 1;\n}\n",
+ "settings.json": "// Zed settings\n//\n// For information on how to configure Zed, see the Zed\n// documentation: https://zed.dev/docs/configuring-zed\n//\n// To see all of Zed's default settings without changing your\n// custom settings, run `zed: open default settings` from the\n// command palette (cmd-shift-p / ctrl-shift-p)\n{\n \"cli_default_open_behavior\": \"existing_window\",\n \"icon_theme\": \"Material Icon Theme\",\n \"auto_install_extensions\": {\n \"dockerfile\": true,\n \"env\": true,\n \"html\": true,\n \"material-icon-theme\": true,\n \"mcp-server-context7\": true,\n \"mcp-server-github\": true,\n \"one-dark-pro\": true,\n \"powershell\": true,\n \"toml\": true\n },\n \"disable_ai\": false,\n \"proxy\": \"\",\n \"agent\": {\n \"sidebar_side\": \"right\",\n \"favorite_models\": [],\n \"model_parameters\": []\n },\n \"project_panel\": {\n \"dock\": \"left\"\n },\n \"telemetry\": {\n \"diagnostics\": false,\n \"metrics\": false,\n \"anthropic_retention\": false\n },\n \"terminal\": {\n \"font_size\": 16.0,\n \"cursor_shape\": \"bar\"\n },\n \"base_keymap\": \"VSCode\",\n \"file_scan_exclusions\": [\n \"**/.git\",\n \"**/.svn\",\n \"**/.hg\",\n \"**/.jj\",\n \"**/CVS\",\n \"**/.DS_Store\",\n \"**/Thumbs.db\",\n \"**/.classpath\",\n \"**/.settings\",\n \"**/.playwright-cli\",\n \"**/output\",\n \"**/.cursor\",\n \"**/.vscode\",\n \"**/.idea\",\n \"**/.fleet\",\n \"**/node_modules\",\n \"**/bun.lockb\",\n \"**/pnpm-lock.yaml\",\n \"**/package-lock.json\",\n \"**/yarn.lock\",\n \"**/dist\",\n \"**/out\",\n \"**/build\",\n \"**/.next\",\n \"**/.nuxt\",\n \"**/.output\",\n \"**/.cache\",\n \"**/.turbo\",\n \"**/.vercel\",\n \"**/.netlify\",\n \"**/tsconfig.tsbuildinfo\",\n \"**/auto-imports.d.ts\",\n \"**/components.json\",\n \"**/unplugin-vue-components.d.ts\",\n \"src/*.d.ts\",\n \"**/__pycache__\",\n \"**/__init__.py\",\n \"**/.ipynb_checkpoints\",\n \"**/.pytest_cache\",\n \"**/.mypy_cache\",\n \"**/.venv\",\n \"**/env\",\n \"**/venv\",\n \"**/.github\",\n \"**/.husky\",\n \"**/.npmrc\",\n \"**/.eslintrc*\",\n \"**/.prettier*\",\n \"**/.editorconfig\",\n \"**/.dockerignore\",\n \"**/docker-compose.override.yml\",\n \"**/cypress*\",\n \"**/prisma/migrations\"\n ],\n \"preview_tabs\": {\n \"enabled\": false\n },\n \"git_panel\": {\n \"fallback_branch_name\": \"master\"\n },\n \"sticky_scroll\": {\n \"enabled\": true\n },\n \"minimap\": {\n \"show\": \"always\"\n },\n \"hover_popover_delay\": 250,\n \"cursor_blink\": true,\n \"autosave\": \"on_focus_change\",\n \"buffer_font_fallbacks\": [\n \"Consolas\",\n \"Sarasa Gothic SC\",\n \"Courier New\",\n \"monospace\"\n ],\n \"buffer_font_family\": \"Fira Code\",\n \"auto_indent_on_paste\": true,\n \"linked_edits\": true,\n \"show_edit_predictions\": true,\n \"line_ending\": \"prefer_lf\",\n \"hard_tabs\": false,\n \"tab_size\": 2,\n \"agent_servers\": {\n \"codex-acp\": {\n \"type\": \"registry\"\n }\n },\n \"ui_font_size\": 16,\n \"buffer_font_size\": 16.0,\n \"theme\": {\n \"mode\": \"system\",\n \"light\": \"One Light\",\n \"dark\": \"One Dark Pro\",\n },\n}\n",
+ "tasks.json": "// Project tasks configuration. See https://zed.dev/docs/tasks for documentation.\r\n//\n// Example:\n[\n /* The generated example task is intentionally disabled.\n {\n \"label\": \"Example task\",\r\n \"command\": \"for i in {1..5}; do echo \\\"Hello $i/5\\\"; sleep 1; done\",\r\n //\"args\": [],\r\n // Env overrides for the command, will be appended to the terminal's environment from the settings.\r\n \"env\": { \"foo\": \"bar\" },\r\n // Current working directory to spawn the command into, defaults to current project root.\r\n //\"cwd\": \"/path/to/working/directory\",\r\n // Whether to use a new terminal tab or reuse the existing one to spawn the process, defaults to `false`.\r\n \"use_new_terminal\": false,\r\n // Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish, defaults to `false`.\r\n \"allow_concurrent_runs\": false,\r\n // What to do with the terminal pane and tab, after the command was started:\r\n // * `always` — always show the task's pane, and focus the corresponding tab in it (default)\r\n // * `no_focus` — always show the task's pane, add the task's tab in it, but don't focus it\r\n // * `never` — do not alter focus, but still add/reuse the task's tab in its pane\r\n \"reveal\": \"always\",\r\n // Where to place the task's terminal item after starting the task:\r\n // * `dock` — in the terminal dock, \"regular\" terminal items' place (default)\r\n // * `center` — in the central pane group, \"main\" editor area\r\n \"reveal_target\": \"dock\",\r\n // What to do with the terminal pane and tab, after the command had finished:\r\n // * `never` — Do nothing when the command finishes (default)\r\n // * `always` — always hide the terminal tab, hide the pane also if it was the last tab in it\r\n // * `on_success` — hide the terminal tab on task success only, otherwise behaves similar to `always`\r\n \"hide\": \"never\",\r\n // Which shell to use when running a task inside the terminal.\r\n // May take 3 values:\r\n // 1. (default) Use the system's default terminal configuration in /etc/passwd\r\n // \"shell\": \"system\"\r\n // 2. A program:\r\n // \"shell\": {\r\n // \"program\": \"sh\"\r\n // }\r\n // 3. A program with arguments:\r\n // \"shell\": {\r\n // \"with_arguments\": {\r\n // \"program\": \"/bin/bash\",\r\n // \"args\": [\"--login\"]\r\n // }\r\n // }\r\n \"shell\": \"system\",\r\n // Whether to show the task line in the output of the spawned task, defaults to `true`.\r\n \"show_summary\": true,\r\n // Whether to show the command line in the output of the spawned task, defaults to `true`.\r\n \"show_command\": true,\r\n // Which edited buffers to save before running the task:\r\n // * `all` — save all edited buffers\r\n // * `current` — save currently active buffer only\r\n // * `none` — don't save any buffers\r\n \"save\": \"none\",\r\n // Represents the tags for inline runnable indicators, or spawning multiple tasks at once.\r\n // \"tags\": []\n }, */\n {\n \"label\": \"Toggle excluded files\",\n \"command\": \"node\",\n \"args\": [\n \"__TOGGLE_SCRIPT__\"\n ],\n \"use_new_terminal\": false,\n \"allow_concurrent_runs\": false,\n \"reveal\": \"never\",\n \"hide\": \"always\",\n \"show_summary\": false,\n \"show_command\": false,\n \"save\": \"none\"\n },\n {\n \"label\": \"Zed Settings: Export JSON...\",\n \"command\": \"node\",\n \"args\": [\n \"__SYNC_SCRIPT__\",\n \"export-dialog\"\n ],\n \"allow_concurrent_runs\": false,\n \"reveal\": \"never\",\n \"hide\": \"always\",\n \"show_summary\": false,\n \"show_command\": false,\n \"save\": \"all\"\n },\n {\n \"label\": \"Zed Settings: Import JSON...\",\n \"command\": \"node\",\n \"args\": [\n \"__SYNC_SCRIPT__\",\n \"import-dialog\"\n ],\n \"allow_concurrent_runs\": false,\n \"reveal\": \"never\",\n \"hide\": \"always\",\n \"show_summary\": false,\n \"show_command\": false,\n \"save\": \"all\"\n },\n {\n \"label\": \"Zed Settings: Configure GitHub Gist...\",\n \"command\": \"node\",\n \"args\": [\n \"__SYNC_SCRIPT__\",\n \"gist-configure\"\n ],\n \"allow_concurrent_runs\": false,\n \"reveal\": \"no_focus\",\n \"hide\": \"never\",\n \"show_summary\": false,\n \"show_command\": false,\n \"save\": \"all\"\n },\n {\n \"label\": \"Zed Settings: Show GitHub Gist Status\",\n \"command\": \"node\",\n \"args\": [\n \"__SYNC_SCRIPT__\",\n \"gist-status\"\n ],\n \"allow_concurrent_runs\": false,\n \"reveal\": \"no_focus\",\n \"hide\": \"never\",\n \"show_summary\": false,\n \"show_command\": false,\n \"save\": \"all\"\n },\n {\n \"label\": \"Zed Settings: Push to GitHub Gist\",\n \"command\": \"node\",\n \"args\": [\n \"__SYNC_SCRIPT__\",\n \"gist-push\"\n ],\n \"allow_concurrent_runs\": false,\n \"reveal\": \"no_focus\",\n \"hide\": \"never\",\n \"show_summary\": false,\n \"show_command\": false,\n \"save\": \"all\"\n },\n {\n \"label\": \"Zed Settings: Pull from GitHub Gist\",\n \"command\": \"node\",\n \"args\": [\n \"__SYNC_SCRIPT__\",\n \"gist-pull\"\n ],\n \"allow_concurrent_runs\": false,\n \"reveal\": \"no_focus\",\n \"hide\": \"never\",\n \"show_summary\": false,\n \"show_command\": false,\n \"save\": \"all\"\n },\n {\n \"label\": \"Zed Settings: Authenticate GitHub...\",\n \"command\": \"powershell\",\n \"args\": [\n \"-NoExit\",\n \"-Command\",\n \"Remove-Item Env:GH_TOKEN,Env:GITHUB_TOKEN -ErrorAction SilentlyContinue; gh auth login\"\n ],\n \"allow_concurrent_runs\": false,\n \"reveal\": \"always\",\n \"hide\": \"never\",\n \"show_summary\": false,\n \"show_command\": true,\n \"save\": \"none\"\n },\n]\n"
+}