Add portable Zed Gist sync configuration
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
backups/
|
||||
*.tmp
|
||||
*.log
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
# Zed AI setup
|
||||
|
||||
## Codex in the Agent Panel
|
||||
|
||||
The live configuration now has `disable_ai: false` and already registers `codex-acp`. Restart or reload Zed, open the ACP Registry, install or confirm Codex, then create a Codex thread from the Agent Panel. Codex owns its authentication and billing; use its native login flow for ChatGPT or API-based authentication supported by the installed version.
|
||||
|
||||
This does not require a VS Code extension. Zed hosts Codex through ACP.
|
||||
|
||||
## Self-hosted or compatible LLM API
|
||||
|
||||
Use an OpenAI-compatible provider for an endpoint implementing OpenAI chat-completions or Responses semantics:
|
||||
|
||||
```jsonc
|
||||
"language_models": {
|
||||
"openai_compatible": {
|
||||
"my-provider": {
|
||||
"api_url": "https://llm.example.com/v1",
|
||||
"available_models": [
|
||||
{
|
||||
"name": "my-model",
|
||||
"display_name": "My Model",
|
||||
"max_tokens": 128000
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Enter the key through **Agent Settings > LLM Providers**, or set `MY_PROVIDER_API_KEY`. Do not put the key in `settings.json`.
|
||||
|
||||
Anthropic-compatible endpoints can be configured under `language_models.anthropic_compatible`. First-class providers include Anthropic, OpenAI, Google AI, Mistral, DeepSeek, xAI, and OpenCode. Zed also supports OpenRouter, Vercel AI Gateway, Bedrock, Ollama, LM Studio, llama.cpp, and local OpenAI-compatible servers.
|
||||
|
||||
Using your own key or local endpoint does not require paying for Zed-hosted models. The API provider may still charge for usage. Zed-hosted models use Zed's plan and usage billing.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Zed portable configuration
|
||||
|
||||
This directory is the portable source for Zed settings, keybindings, tasks, extension installation preferences, and the file-exclusion toggle.
|
||||
|
||||
## 一行安装
|
||||
|
||||
前置依赖:已安装 Node.js 和 GitHub CLI `gh`。
|
||||
|
||||
Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
irm https://git.okk.cool/purp1e/zed-sync/raw/branch/master/bootstrap-from-gist.ps1 | iex
|
||||
```
|
||||
|
||||
macOS/Linux:
|
||||
|
||||
```sh
|
||||
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。
|
||||
|
||||
For first-machine setup, new-machine onboarding, daily Gist synchronization, and conflict handling, see [`SYNC-MANUAL.zh-CN.md`](./SYNC-MANUAL.zh-CN.md).
|
||||
|
||||
## Files
|
||||
|
||||
- `settings.json`: editor settings and extension installation list. Provider secrets are not stored here.
|
||||
- `keymap.json`: cross-platform bindings using `secondary-` for Command/Ctrl.
|
||||
- `tasks.json`: task template. Script-path placeholders are rendered during installation.
|
||||
- `scripts/file-exclusions.json`: the only file to edit when changing permanent or toggleable exclusion rules.
|
||||
- `scripts/toggle-file-scan-exclusions.mjs`: updates `file_scan_exclusions` in the active Zed settings.
|
||||
- `scripts/settings-sync.json`: machine-local Gist ID and last synchronized hash. Export and import never overwrite it.
|
||||
- `scripts/zed-settings-sync.mjs`: single-JSON import, export, hashing, and Gist synchronization.
|
||||
- `bootstrap-from-gist.ps1` / `bootstrap-from-gist.sh`: standalone one-line installers for a fresh machine. They download the single JSON directly from Gist and do not need the rest of this directory.
|
||||
|
||||
## Zed Task Picker commands
|
||||
|
||||
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.
|
||||
- `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.
|
||||
- `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.
|
||||
|
||||
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.
|
||||
|
||||
A secret Gist is unlisted, not access-controlled: anyone who obtains its URL can read it. This bundle deliberately excludes API keys, but a synchronized folder or private Git repository is the safer choice for configuration that contains sensitive paths or identifiers.
|
||||
|
||||
## Installed keybindings
|
||||
|
||||
- `Mod+R`: open the Task Picker; search for `Zed Settings` to import, export, push, or pull.
|
||||
- `Mod+; U`: upload the current single JSON to the configured Gist.
|
||||
- `Mod+; D`: download and import the single JSON from the configured Gist.
|
||||
- `Mod+9` / `Mod+0`: activate the previous / next tab in the current pane.
|
||||
- `Alt+1` through `Alt+5`: activate editor pane 1 through 5.
|
||||
- `Mod+Shift+R`: switch to a recent project.
|
||||
- `Mod+; K`: reload Zed.
|
||||
- `Mod+; C`: focus the Git panel; press it again to focus the commit-message editor.
|
||||
|
||||
`Mod` is Command on macOS and Ctrl on Windows/Linux because the keymap uses Zed's `secondary-` syntax. `Alt` maps to Option on macOS.
|
||||
|
||||
## Windows
|
||||
|
||||
Install this copy into Zed:
|
||||
|
||||
```powershell
|
||||
.\install.ps1
|
||||
```
|
||||
|
||||
Export later changes from Zed back into this directory:
|
||||
|
||||
```powershell
|
||||
.\export.ps1
|
||||
```
|
||||
|
||||
## macOS and Linux
|
||||
|
||||
Node.js is required for the exclusion toggle and task-template rendering.
|
||||
|
||||
```sh
|
||||
sh ./install.sh
|
||||
sh ./export.sh
|
||||
```
|
||||
|
||||
## Private Git synchronization
|
||||
|
||||
Initialize this directory once and push it to a private remote:
|
||||
|
||||
```sh
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Add portable Zed configuration"
|
||||
git remote add origin <private-repository-url>
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
On another machine, clone the repository and run the platform installer. After changing Zed settings, run the export script, review `git diff`, then commit and push. On other machines, pull and run the installer again.
|
||||
|
||||
Zed account login does not currently synchronize these files. API keys remain in each machine's system keychain or environment variables and must not be committed.
|
||||
@@ -0,0 +1,256 @@
|
||||
# Zed 设置同步使用手册
|
||||
|
||||
本方案使用 GitHub Gist 中的单个 `zed-settings-sync.json` 同步 Zed 设置。JSON 顶层直接保存各配置文件的路径和完整内容;SHA-256 只在运行时计算,不写入同步文件。
|
||||
|
||||
## 1. 同步内容
|
||||
|
||||
会同步:
|
||||
|
||||
- `settings.json`:界面、字体、主题、编辑器行为及扩展自动安装清单。
|
||||
- `keymap.json`:自定义键位。
|
||||
- `tasks.json`:文件显隐和设置同步任务。
|
||||
- `scripts/` 中的文件显隐及同步脚本。
|
||||
- 非空的 `snippets/` 和 `themes/` 目录。
|
||||
|
||||
不会同步:
|
||||
|
||||
- GitHub 登录凭据、API Key 和系统环境变量。
|
||||
- 本机保存的 Gist ID 和上次同步哈希。
|
||||
- Zed 数据库、缓存、聊天记录、项目文件及扩展二进制。
|
||||
|
||||
扩展 ID 位于 `settings.json` 的 `auto_install_extensions` 中。导入后由 Zed 在目标平台重新安装扩展。
|
||||
|
||||
## 2. 准备 Gist
|
||||
|
||||
1. 登录 GitHub,打开 <https://gist.github.com/>。
|
||||
2. 创建一个 Secret Gist。GitHub 不允许创建完全空白的 Gist,可以先放一个 `README.md`。
|
||||
3. 复制完整 Gist URL 或末尾的 Gist ID。
|
||||
|
||||
Gist ID 不是访问令牌。身份认证由 GitHub CLI `gh` 管理。
|
||||
|
||||
Secret Gist 只是不出现在公开搜索中,获得链接的人仍然可以读取其内容。同步 JSON 不包含 API Key,但仍应妥善保管 Gist URL。
|
||||
|
||||
## 3. 主机器首次上传
|
||||
|
||||
### 3.1 安装和认证 GitHub CLI
|
||||
|
||||
当前 Windows 机器已经安装 `gh`。其他机器可以使用系统包管理器安装:
|
||||
|
||||
```powershell
|
||||
winget install GitHub.cli
|
||||
```
|
||||
|
||||
macOS:
|
||||
|
||||
```sh
|
||||
brew install gh
|
||||
```
|
||||
|
||||
安装后,在 Zed 中按 `Mod+R` 打开 Task Picker,运行:
|
||||
|
||||
```text
|
||||
Zed Settings: Authenticate GitHub...
|
||||
```
|
||||
|
||||
也可以在终端执行:
|
||||
|
||||
```sh
|
||||
gh auth login
|
||||
```
|
||||
|
||||
### 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`。
|
||||
|
||||
Push 只会在该 Gist 中创建或更新 `zed-settings-sync.json`,不会删除 Gist 中的其他文件。
|
||||
|
||||
Gist ID 和上次同步哈希保存在本机:
|
||||
|
||||
```text
|
||||
Windows: %APPDATA%\Zed\scripts\settings-sync.json
|
||||
macOS/Linux: ~/.config/zed/scripts/settings-sync.json
|
||||
```
|
||||
|
||||
此文件不参与导入导出,所以其他机器不会覆盖当前机器记录。
|
||||
|
||||
## 4. 新机器首次接入
|
||||
|
||||
第一次接入只需要运行仓库里的独立 bootstrap 脚本。它会询问 Gist URL/ID,直接读取 Gist 单 JSON,再安装全部设置和同步工具;不需要下载或解压便携目录。
|
||||
|
||||
### 4.1 安装前置程序
|
||||
|
||||
新机器需要:
|
||||
|
||||
- Zed 或 ZedG。
|
||||
- Node.js。
|
||||
- GitHub CLI `gh`。
|
||||
|
||||
bootstrap 仓库本身不保存设置或凭据,只需要包含:
|
||||
|
||||
```text
|
||||
bootstrap-from-gist.ps1
|
||||
bootstrap-from-gist.sh
|
||||
```
|
||||
|
||||
因此这个仓库可以公开;真正的设置仍只存放在指定 Gist 中。
|
||||
|
||||
### 4.2 一行安装
|
||||
|
||||
Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
irm https://git.okk.cool/purp1e/zed-sync/raw/branch/master/bootstrap-from-gist.ps1 | iex
|
||||
```
|
||||
|
||||
macOS/Linux:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://git.okk.cool/purp1e/zed-sync/raw/branch/master/bootstrap-from-gist.sh | sh
|
||||
```
|
||||
|
||||
迁移到其他 Gitea 仓库时,Raw URL 的常见形式是:
|
||||
|
||||
```powershell
|
||||
irm https://<Gitea域名>/<用户>/<仓库>/raw/branch/main/bootstrap-from-gist.ps1 | iex
|
||||
```
|
||||
|
||||
```sh
|
||||
curl -fsSL https://<Gitea域名>/<用户>/<仓库>/raw/branch/main/bootstrap-from-gist.sh | sh
|
||||
```
|
||||
|
||||
执行过程中脚本会:
|
||||
|
||||
1. 检查 Node.js 和 `gh`。
|
||||
2. 在尚未登录时运行 `gh auth login`。
|
||||
3. 询问 Gist URL 或 ID。
|
||||
4. 从 GitHub API 读取 `zed-settings-sync.json`。
|
||||
5. 验证顶层文件路径,防止写出 Zed 配置目录。
|
||||
6. 备份新机器已有配置。
|
||||
7. 安装设置、键位、Tasks、snippets、themes 和辅助脚本。
|
||||
8. 保存 Gist ID 和远端整文件 SHA-256 作为首次同步基线。
|
||||
|
||||
安装完成后重启 Zed,运行 `Zed Settings: Show GitHub Gist Status`,状态应为 `Synchronized`。
|
||||
|
||||
为了降低执行远程脚本的供应链风险,可以把 URL 中的 `main` 换成审核过的 commit SHA。bootstrap 仓库不应加入访问令牌或其他秘密。
|
||||
|
||||
### 4.3 便携目录备用安装
|
||||
|
||||
不能访问 Raw URL 时,仍可复制完整 `zed-config-portable` 目录并运行:
|
||||
|
||||
```powershell
|
||||
.\install.ps1
|
||||
```
|
||||
|
||||
```sh
|
||||
sh ./install.sh
|
||||
```
|
||||
|
||||
然后在 Task Picker 中认证 GitHub、配置 Gist 并执行首次 Pull。
|
||||
|
||||
## 5. 之后的日常使用
|
||||
|
||||
开始在一台机器工作前:
|
||||
|
||||
1. 按 `Mod+R`。
|
||||
2. 运行 `Zed Settings: Show GitHub Gist Status`。
|
||||
3. 如果显示 `Remote changes available`,执行 Pull。
|
||||
|
||||
修改设置、键位、Tasks、排除规则或 snippets 后:
|
||||
|
||||
1. 执行 Push。
|
||||
2. 确认状态变为 `Synchronized`。
|
||||
3. 再到其他机器执行 Pull。
|
||||
|
||||
常用快捷键:
|
||||
|
||||
| 快捷键 | 操作 |
|
||||
|---|---|
|
||||
| `Mod+R` | 打开 Task Picker |
|
||||
| `Mod+; U` | Push 到 Gist |
|
||||
| `Mod+; D` | 从 Gist Pull |
|
||||
| `Mod+; K` | Reload Zed |
|
||||
|
||||
`Mod` 在 Windows/Linux 是 Ctrl,在 macOS 是 Command。
|
||||
|
||||
## 6. 哈希和冲突处理
|
||||
|
||||
系统比较三个 SHA-256:
|
||||
|
||||
- 当前机器实时生成的单 JSON 哈希。
|
||||
- Gist 中单 JSON 的哈希。
|
||||
- 当前机器上次成功 Push/Pull 时保存的哈希。
|
||||
|
||||
Status 可能显示:
|
||||
|
||||
| 状态 | 含义 | 建议操作 |
|
||||
|---|---|---|
|
||||
| `Synchronized` | 本地和 Gist 一致 | 无需操作 |
|
||||
| `Local changes pending upload` | 只有本机有变化 | Push |
|
||||
| `Remote changes available` | 只有 Gist 有变化 | Pull |
|
||||
| `Conflict` | 本机和 Gist 都有变化 | 先导出本地备份,再决定 Push 或 Pull |
|
||||
| `Not synchronized on this machine` | 这台机器尚无同步基线 | 首次接入通常选择 Pull |
|
||||
|
||||
Push 覆盖远端前、Pull 覆盖本地前都会显示哈希并要求确认。Pull 会自动备份本地配置;系统不会尝试逐字段自动合并。
|
||||
|
||||
## 7. 离线 JSON 导入导出
|
||||
|
||||
不使用 Gist 时,也可以使用完全相同的单 JSON 格式:
|
||||
|
||||
```text
|
||||
Zed Settings: Export JSON...
|
||||
Zed Settings: Import JSON...
|
||||
```
|
||||
|
||||
两个命令都会弹出系统文件选择窗口。直接 Import 不会修改已保存的 Gist ID 和上次同步哈希;导入内容会被视为新的本地变化,可以随后 Push 到 Gist。
|
||||
|
||||
## 8. 修改文件排除规则
|
||||
|
||||
只编辑本机配置中的 `custom` 数组:
|
||||
|
||||
```text
|
||||
Windows: %APPDATA%\Zed\scripts\file-exclusions.json
|
||||
macOS/Linux: ~/.config/zed/scripts/file-exclusions.json
|
||||
```
|
||||
|
||||
修改后按 `Alt+Shift+-` 测试显隐,再 Push 到 Gist。其他机器 Pull 后会取得相同规则。
|
||||
|
||||
## 9. 备份位置
|
||||
|
||||
每次 Import 或 Pull 前的备份位于:
|
||||
|
||||
```text
|
||||
Windows: %APPDATA%\Zed\backups\sync-import-<时间>
|
||||
macOS/Linux: ~/.config/zed/backups/sync-import-<时间>
|
||||
```
|
||||
|
||||
便携安装器产生的备份使用 `portable-install-<时间>` 名称。
|
||||
|
||||
## 10. 常见问题
|
||||
|
||||
### Task Picker 中没有 Zed Settings
|
||||
|
||||
重新运行一行 bootstrap 命令,或运行便携目录中的 `install.ps1` / `install.sh`,然后 Reload Zed。
|
||||
|
||||
### 提示找不到 gh
|
||||
|
||||
安装 GitHub CLI 后完全退出并重启 Zed,使编辑器读取更新后的 PATH。
|
||||
|
||||
### Gist 返回 404
|
||||
|
||||
检查 Gist URL/ID、当前 GitHub 账号以及 Gist 是否仍然存在。重新运行 `Configure GitHub Gist...` 可以更换记录。
|
||||
|
||||
### Gist 中没有 zed-settings-sync.json
|
||||
|
||||
先在已经配置好的主机器上执行一次 Push。
|
||||
|
||||
### Pull 后键位或界面没有立即变化
|
||||
|
||||
执行 `Mod+; K` Reload Zed;如果扩展仍在安装,等待完成后再重启一次。
|
||||
|
||||
### 换了另一个 Gist
|
||||
|
||||
运行 `Configure GitHub Gist...` 并输入新 ID。系统会保留新 ID,同时清空旧 Gist 对应的上次同步哈希,下一次 Push/Pull 会重新确认覆盖方向。
|
||||
@@ -0,0 +1,134 @@
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$bundleName = "zed-settings-sync.json"
|
||||
$utf8 = New-Object System.Text.UTF8Encoding($false)
|
||||
|
||||
function Require-Command([string]$Name, [string]$InstallHint) {
|
||||
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
|
||||
throw "$Name is required. Install it first: $InstallHint"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-GistId([string]$Value) {
|
||||
$matches = [regex]::Matches($Value.Trim(), "[a-fA-F0-9]{8,}")
|
||||
if ($matches.Count -eq 0) {
|
||||
throw "Enter a valid GitHub Gist ID or URL."
|
||||
}
|
||||
return $matches[$matches.Count - 1].Value.ToLowerInvariant()
|
||||
}
|
||||
|
||||
function Test-AllowedPath([string]$Path) {
|
||||
$fixed = @(
|
||||
"settings.json",
|
||||
"keymap.json",
|
||||
"tasks.json",
|
||||
"debug.json",
|
||||
"scripts/toggle-file-scan-exclusions.mjs",
|
||||
"scripts/file-exclusions.json",
|
||||
"scripts/zed-settings-sync.mjs"
|
||||
)
|
||||
if ($Path.StartsWith("/") -or $Path.Contains("\") -or $Path.Split("/") -contains "..") {
|
||||
return $false
|
||||
}
|
||||
return $fixed -contains $Path -or $Path.StartsWith("snippets/") -or $Path.StartsWith("themes/")
|
||||
}
|
||||
|
||||
function Write-Atomic([string]$Path, [string]$Contents) {
|
||||
$directory = Split-Path $Path -Parent
|
||||
New-Item -ItemType Directory -Force -Path $directory | Out-Null
|
||||
$temporary = Join-Path $directory ".$([IO.Path]::GetFileName($Path)).$PID.tmp"
|
||||
try {
|
||||
[IO.File]::WriteAllText($temporary, $Contents, $utf8)
|
||||
Move-Item -LiteralPath $temporary -Destination $Path -Force
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
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." }
|
||||
}
|
||||
|
||||
$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
|
||||
if ($env:ZED_SETTINGS_BUNDLE_PATH) {
|
||||
$bundleSource = [IO.File]::ReadAllText($env:ZED_SETTINGS_BUNDLE_PATH, [Text.Encoding]::UTF8)
|
||||
} else {
|
||||
$token = (& gh auth token).Trim()
|
||||
if ($LASTEXITCODE -ne 0 -or -not $token) { throw "Could not obtain the GitHub authentication token from gh." }
|
||||
$headers = @{
|
||||
Accept = "application/vnd.github+json"
|
||||
Authorization = "Bearer $token"
|
||||
"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
|
||||
}
|
||||
|
||||
try {
|
||||
$bundle = $bundleSource | ConvertFrom-Json
|
||||
} catch {
|
||||
throw "The Gist contains invalid JSON: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
$properties = @($bundle.PSObject.Properties)
|
||||
foreach ($required in @("settings.json", "keymap.json", "tasks.json")) {
|
||||
if (-not ($properties.Name -contains $required)) { throw "The bundle is missing $required." }
|
||||
}
|
||||
foreach ($property in $properties) {
|
||||
if (-not (Test-AllowedPath $property.Name) -or $property.Value -isnot [string]) {
|
||||
throw "Invalid bundle file: $($property.Name)"
|
||||
}
|
||||
}
|
||||
|
||||
$target = Join-Path $env:APPDATA "Zed"
|
||||
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$backup = Join-Path $target "backups\gist-bootstrap-$timestamp"
|
||||
New-Item -ItemType Directory -Force -Path $target, $backup, (Join-Path $target "scripts") | Out-Null
|
||||
|
||||
foreach ($property in $properties) {
|
||||
$existing = Join-Path $target ($property.Name.Replace("/", "\"))
|
||||
if (Test-Path -LiteralPath $existing) {
|
||||
$backupPath = Join-Path $backup ($property.Name.Replace("/", "\"))
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path $backupPath -Parent) | Out-Null
|
||||
Copy-Item -LiteralPath $existing -Destination $backupPath -Force
|
||||
}
|
||||
}
|
||||
|
||||
$toggleScript = (Join-Path $target "scripts\toggle-file-scan-exclusions.mjs").Replace("\", "/")
|
||||
$syncScript = (Join-Path $target "scripts\zed-settings-sync.mjs").Replace("\", "/")
|
||||
foreach ($property in $properties) {
|
||||
$destination = Join-Path $target ($property.Name.Replace("/", "\"))
|
||||
$contents = [string]$property.Value
|
||||
if ($property.Name -eq "tasks.json") {
|
||||
$contents = $contents.Replace("__TOGGLE_SCRIPT__", $toggleScript).Replace("__SYNC_SCRIPT__", $syncScript)
|
||||
}
|
||||
Write-Atomic $destination $contents
|
||||
}
|
||||
|
||||
$bytes = $utf8.GetBytes($bundleSource)
|
||||
$sha = [Security.Cryptography.SHA256]::Create()
|
||||
try {
|
||||
$hash = -join ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString("x2") })
|
||||
} finally {
|
||||
$sha.Dispose()
|
||||
}
|
||||
$syncState = [ordered]@{ gist_id = $gistId; last_synced_hash = $hash } | ConvertTo-Json
|
||||
Write-Atomic (Join-Path $target "scripts\settings-sync.json") "$syncState`n"
|
||||
|
||||
Write-Host "Installed Zed settings from Gist $gistId"
|
||||
Write-Host "SHA-256: $hash"
|
||||
Write-Host "Backup: $backup"
|
||||
Write-Host "Restart Zed to load the synchronized configuration."
|
||||
@@ -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
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$source = Join-Path $env:APPDATA "Zed"
|
||||
$target = $PSScriptRoot
|
||||
$utf8 = New-Object System.Text.UTF8Encoding($false)
|
||||
|
||||
Copy-Item -LiteralPath (Join-Path $source "settings.json") -Destination (Join-Path $target "settings.json") -Force
|
||||
Copy-Item -LiteralPath (Join-Path $source "keymap.json") -Destination (Join-Path $target "keymap.json") -Force
|
||||
Copy-Item -LiteralPath (Join-Path $source "scripts\toggle-file-scan-exclusions.mjs") -Destination (Join-Path $target "scripts\toggle-file-scan-exclusions.mjs") -Force
|
||||
Copy-Item -LiteralPath (Join-Path $source "scripts\file-exclusions.json") -Destination (Join-Path $target "scripts\file-exclusions.json") -Force
|
||||
Copy-Item -LiteralPath (Join-Path $source "scripts\zed-settings-sync.mjs") -Destination (Join-Path $target "scripts\zed-settings-sync.mjs") -Force
|
||||
|
||||
$tasks = [IO.File]::ReadAllText((Join-Path $source "tasks.json"))
|
||||
foreach ($script in @(
|
||||
@{ Path = (Join-Path $source "scripts\toggle-file-scan-exclusions.mjs"); Placeholder = "__TOGGLE_SCRIPT__" },
|
||||
@{ Path = (Join-Path $source "scripts\zed-settings-sync.mjs"); Placeholder = "__SYNC_SCRIPT__" }
|
||||
)) {
|
||||
$tasks = $tasks.Replace($script.Path.Replace("\", "\\"), $script.Placeholder)
|
||||
$tasks = $tasks.Replace($script.Path.Replace("\", "/"), $script.Placeholder)
|
||||
}
|
||||
[IO.File]::WriteAllText((Join-Path $target "tasks.json"), $tasks, $utf8)
|
||||
|
||||
Write-Host "Exported Zed configuration from $source"
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
target=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
source="${XDG_CONFIG_HOME:-$HOME/.config}/zed"
|
||||
|
||||
cp "$source/settings.json" "$target/settings.json"
|
||||
cp "$source/keymap.json" "$target/keymap.json"
|
||||
cp "$source/scripts/toggle-file-scan-exclusions.mjs" "$target/scripts/toggle-file-scan-exclusions.mjs"
|
||||
cp "$source/scripts/file-exclusions.json" "$target/scripts/file-exclusions.json"
|
||||
cp "$source/scripts/zed-settings-sync.mjs" "$target/scripts/zed-settings-sync.mjs"
|
||||
|
||||
node - "$source/tasks.json" "$target/tasks.json" "$source/scripts/toggle-file-scan-exclusions.mjs" "$source/scripts/zed-settings-sync.mjs" <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const [source, target, toggleScript, syncScript] = process.argv.slice(2);
|
||||
const tasks = fs.readFileSync(source, "utf8")
|
||||
.replaceAll(toggleScript, "__TOGGLE_SCRIPT__")
|
||||
.replaceAll(syncScript, "__SYNC_SCRIPT__");
|
||||
fs.writeFileSync(target, tasks);
|
||||
NODE
|
||||
|
||||
printf 'Exported Zed configuration from %s\n' "$source"
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$source = $PSScriptRoot
|
||||
$target = Join-Path $env:APPDATA "Zed"
|
||||
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$backup = Join-Path $target "backups\portable-install-$timestamp"
|
||||
$utf8 = New-Object System.Text.UTF8Encoding($false)
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $target, $backup, (Join-Path $target "scripts") | Out-Null
|
||||
|
||||
foreach ($name in @("settings.json", "keymap.json", "tasks.json")) {
|
||||
$existing = Join-Path $target $name
|
||||
if (Test-Path -LiteralPath $existing) {
|
||||
Copy-Item -LiteralPath $existing -Destination (Join-Path $backup $name)
|
||||
}
|
||||
}
|
||||
|
||||
Copy-Item -LiteralPath (Join-Path $source "settings.json") -Destination (Join-Path $target "settings.json") -Force
|
||||
Copy-Item -LiteralPath (Join-Path $source "keymap.json") -Destination (Join-Path $target "keymap.json") -Force
|
||||
Copy-Item -LiteralPath (Join-Path $source "scripts\toggle-file-scan-exclusions.mjs") -Destination (Join-Path $target "scripts\toggle-file-scan-exclusions.mjs") -Force
|
||||
Copy-Item -LiteralPath (Join-Path $source "scripts\file-exclusions.json") -Destination (Join-Path $target "scripts\file-exclusions.json") -Force
|
||||
Copy-Item -LiteralPath (Join-Path $source "scripts\zed-settings-sync.mjs") -Destination (Join-Path $target "scripts\zed-settings-sync.mjs") -Force
|
||||
$syncConfig = Join-Path $target "scripts\settings-sync.json"
|
||||
if (-not (Test-Path -LiteralPath $syncConfig)) {
|
||||
Copy-Item -LiteralPath (Join-Path $source "scripts\settings-sync.json") -Destination $syncConfig
|
||||
}
|
||||
|
||||
$toggleScriptPath = (Join-Path $target "scripts\toggle-file-scan-exclusions.mjs").Replace("\", "/")
|
||||
$syncScriptPath = (Join-Path $target "scripts\zed-settings-sync.mjs").Replace("\", "/")
|
||||
$tasks = [IO.File]::ReadAllText((Join-Path $source "tasks.json"))
|
||||
$tasks = $tasks.Replace("__TOGGLE_SCRIPT__", $toggleScriptPath)
|
||||
$tasks = $tasks.Replace("__SYNC_SCRIPT__", $syncScriptPath)
|
||||
[IO.File]::WriteAllText((Join-Path $target "tasks.json"), $tasks, $utf8)
|
||||
|
||||
Write-Host "Installed Zed configuration to $target"
|
||||
Write-Host "Previous configuration backed up to $backup"
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
source_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
target="${XDG_CONFIG_HOME:-$HOME/.config}/zed"
|
||||
timestamp=$(date +%Y%m%d-%H%M%S)
|
||||
backup="$target/backups/portable-install-$timestamp"
|
||||
|
||||
mkdir -p "$target/scripts" "$backup"
|
||||
for name in settings.json keymap.json tasks.json; do
|
||||
if [ -f "$target/$name" ]; then
|
||||
cp "$target/$name" "$backup/$name"
|
||||
fi
|
||||
done
|
||||
|
||||
cp "$source_dir/settings.json" "$target/settings.json"
|
||||
cp "$source_dir/keymap.json" "$target/keymap.json"
|
||||
cp "$source_dir/scripts/toggle-file-scan-exclusions.mjs" "$target/scripts/toggle-file-scan-exclusions.mjs"
|
||||
cp "$source_dir/scripts/file-exclusions.json" "$target/scripts/file-exclusions.json"
|
||||
cp "$source_dir/scripts/zed-settings-sync.mjs" "$target/scripts/zed-settings-sync.mjs"
|
||||
if [ ! -f "$target/scripts/settings-sync.json" ]; then
|
||||
cp "$source_dir/scripts/settings-sync.json" "$target/scripts/settings-sync.json"
|
||||
fi
|
||||
|
||||
node - "$source_dir/tasks.json" "$target/tasks.json" "$target/scripts/toggle-file-scan-exclusions.mjs" "$target/scripts/zed-settings-sync.mjs" <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const [source, target, toggleScript, syncScript] = process.argv.slice(2);
|
||||
const tasks = fs.readFileSync(source, "utf8")
|
||||
.replaceAll("__TOGGLE_SCRIPT__", toggleScript)
|
||||
.replaceAll("__SYNC_SCRIPT__", syncScript);
|
||||
fs.writeFileSync(target, tasks);
|
||||
NODE
|
||||
|
||||
printf 'Installed Zed configuration to %s\n' "$target"
|
||||
printf 'Previous configuration backed up to %s\n' "$backup"
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"f12": ["zed::IncreaseUiFontSize", { "persist": false }],
|
||||
"secondary-f12": ["zed::DecreaseUiFontSize", { "persist": false }],
|
||||
"secondary-1": "project_panel::ToggleFocus",
|
||||
"secondary-2": "terminal_panel::ToggleFocus",
|
||||
"secondary-3": "workspace::ToggleRightDock",
|
||||
"secondary-shift-g": "git_panel::ToggleFocus",
|
||||
"secondary-shift-v": "zed::Extensions",
|
||||
"secondary-p": "command_palette::Toggle",
|
||||
"secondary-o": "file_finder::Toggle",
|
||||
"secondary-9": "pane::ActivatePreviousItem",
|
||||
"secondary-0": "pane::ActivateNextItem",
|
||||
"alt-1": ["workspace::ActivatePane", 0],
|
||||
"alt-2": ["workspace::ActivatePane", 1],
|
||||
"alt-3": ["workspace::ActivatePane", 2],
|
||||
"alt-4": ["workspace::ActivatePane", 3],
|
||||
"alt-5": ["workspace::ActivatePane", 4],
|
||||
"secondary-shift-o": "workspace::Open",
|
||||
"secondary-enter": "workspace::ToggleZoom",
|
||||
"secondary-r": "task::Spawn",
|
||||
"secondary-shift-r": "projects::OpenRecent",
|
||||
"shift-alt-r": null,
|
||||
"secondary-; k": "workspace::Reload",
|
||||
"secondary-; g": "git::FileHistory",
|
||||
"secondary-; p": "git::Push",
|
||||
"secondary-; secondary-p": "git::Pull",
|
||||
"secondary-; n": "git::Branch",
|
||||
"secondary-; s": "git::StashAll",
|
||||
"secondary-; z": "git::StashPop",
|
||||
"secondary-; a": "git::StageAll",
|
||||
"secondary-; u": [
|
||||
"task::Spawn",
|
||||
{ "task_name": "Zed Settings: Push to GitHub Gist" }
|
||||
],
|
||||
"secondary-; d": [
|
||||
"task::Spawn",
|
||||
{ "task_name": "Zed Settings: Pull from GitHub Gist" }
|
||||
],
|
||||
"shift-alt--": [
|
||||
"task::Spawn",
|
||||
{ "task_name": "Toggle excluded files" }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"secondary-; c": "git_panel::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitPanel",
|
||||
"bindings": {
|
||||
"secondary-; c": "git_panel::FocusEditor"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"secondary-shift-[": "editor::Fold",
|
||||
"secondary-shift-]": "editor::UnfoldLines",
|
||||
"secondary--": "editor::FoldAll",
|
||||
"secondary-=": "editor::UnfoldAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && extension == md",
|
||||
"bindings": {
|
||||
"alt-p": "markdown::OpenPreview",
|
||||
"shift-alt-p": "markdown::OpenPreviewToTheSide"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {
|
||||
"alt--": "project_panel::CollapseAllEntries"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"defaults": [
|
||||
"**/.git",
|
||||
"**/.svn",
|
||||
"**/.hg",
|
||||
"**/.jj",
|
||||
"**/CVS",
|
||||
"**/.DS_Store",
|
||||
"**/Thumbs.db",
|
||||
"**/.classpath",
|
||||
"**/.settings"
|
||||
],
|
||||
"custom": [
|
||||
"**/.playwright-cli",
|
||||
"**/output",
|
||||
"**/.cursor",
|
||||
"**/.vscode",
|
||||
"**/.idea",
|
||||
"**/.fleet",
|
||||
"**/node_modules",
|
||||
"**/bun.lockb",
|
||||
"**/pnpm-lock.yaml",
|
||||
"**/package-lock.json",
|
||||
"**/yarn.lock",
|
||||
"**/dist",
|
||||
"**/out",
|
||||
"**/build",
|
||||
"**/.next",
|
||||
"**/.nuxt",
|
||||
"**/.output",
|
||||
"**/.cache",
|
||||
"**/.turbo",
|
||||
"**/.vercel",
|
||||
"**/.netlify",
|
||||
"**/tsconfig.tsbuildinfo",
|
||||
"**/auto-imports.d.ts",
|
||||
"**/components.json",
|
||||
"**/unplugin-vue-components.d.ts",
|
||||
"src/*.d.ts",
|
||||
"**/__pycache__",
|
||||
"**/__init__.py",
|
||||
"**/.ipynb_checkpoints",
|
||||
"**/.pytest_cache",
|
||||
"**/.mypy_cache",
|
||||
"**/.venv",
|
||||
"**/env",
|
||||
"**/venv",
|
||||
"**/.github",
|
||||
"**/.husky",
|
||||
"**/.npmrc",
|
||||
"**/.eslintrc*",
|
||||
"**/.prettier*",
|
||||
"**/.editorconfig",
|
||||
"**/.dockerignore",
|
||||
"**/docker-compose.override.yml",
|
||||
"**/cypress*",
|
||||
"**/prisma/migrations"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"gist_id": "",
|
||||
"last_synced_hash": ""
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { readFile, rename, rm, writeFile } from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
async function exclusionRules() {
|
||||
const rulesPath = join(scriptDirectory, "file-exclusions.json");
|
||||
const rules = JSON.parse(await readFile(rulesPath, "utf8"));
|
||||
if (!Array.isArray(rules.defaults) || !Array.isArray(rules.custom)) {
|
||||
throw new Error("file-exclusions.json must contain defaults and custom arrays");
|
||||
}
|
||||
return {
|
||||
defaults: [...new Set(rules.defaults)],
|
||||
custom: [...new Set(rules.custom)].filter(
|
||||
(entry) => !rules.defaults.includes(entry),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function settingsPath() {
|
||||
if (process.platform === "win32") {
|
||||
const appData = process.env.APPDATA;
|
||||
if (!appData) throw new Error("APPDATA is not set");
|
||||
return join(appData, "Zed", "settings.json");
|
||||
}
|
||||
return join(homedir(), ".config", "zed", "settings.json");
|
||||
}
|
||||
|
||||
function findArrayRange(source) {
|
||||
const property = /"file_scan_exclusions"\s*:/g.exec(source);
|
||||
if (!property) throw new Error("file_scan_exclusions is missing from settings.json");
|
||||
|
||||
const start = source.indexOf("[", property.index + property[0].length);
|
||||
if (start === -1) throw new Error("file_scan_exclusions is not an array");
|
||||
|
||||
let inString = false;
|
||||
let escaped = false;
|
||||
let depth = 0;
|
||||
for (let index = start; index < source.length; index += 1) {
|
||||
const character = source[index];
|
||||
if (inString) {
|
||||
if (escaped) escaped = false;
|
||||
else if (character === "\\") escaped = true;
|
||||
else if (character === '"') inString = false;
|
||||
continue;
|
||||
}
|
||||
if (character === '"') inString = true;
|
||||
else if (character === "[") depth += 1;
|
||||
else if (character === "]" && --depth === 0) return { start, end: index + 1 };
|
||||
}
|
||||
throw new Error("file_scan_exclusions array is not closed");
|
||||
}
|
||||
|
||||
function formatArray(values, indent, newline) {
|
||||
const itemIndent = `${indent} `;
|
||||
const items = values.map((value) => `${itemIndent}${JSON.stringify(value)}`);
|
||||
return `[${newline}${items.join(`,${newline}`)}${newline}${indent}]`;
|
||||
}
|
||||
|
||||
async function replaceAtomically(path, contents) {
|
||||
const temporaryPath = join(dirname(path), `.settings.json.${process.pid}.tmp`);
|
||||
try {
|
||||
await writeFile(temporaryPath, contents, "utf8");
|
||||
await rename(temporaryPath, path);
|
||||
} finally {
|
||||
await rm(temporaryPath, { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const path = settingsPath();
|
||||
const { defaults, custom } = await exclusionRules();
|
||||
const source = await readFile(path, "utf8");
|
||||
const newline = source.includes("\r\n") ? "\r\n" : "\n";
|
||||
const range = findArrayRange(source);
|
||||
const current = JSON.parse(source.slice(range.start, range.end));
|
||||
const customIsActive = current.some((entry) => !defaults.includes(entry));
|
||||
const next = customIsActive ? defaults : [...defaults, ...custom];
|
||||
const lineStart = source.lastIndexOf("\n", range.start) + 1;
|
||||
const indent = source.slice(lineStart, range.start).match(/^\s*/)?.[0] ?? "";
|
||||
const updated =
|
||||
source.slice(0, range.start) +
|
||||
formatArray(next, indent, newline) +
|
||||
source.slice(range.end);
|
||||
|
||||
await replaceAtomically(path, updated);
|
||||
console.log(`Excluded files are now ${customIsActive ? "shown" : "hidden"}.`);
|
||||
@@ -0,0 +1,440 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { copyFile, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const BUNDLE_NAME = "zed-settings-sync.json";
|
||||
const REQUIRED_FILES = ["settings.json", "keymap.json", "tasks.json"];
|
||||
const FIXED_FILES = [
|
||||
...REQUIRED_FILES,
|
||||
"debug.json",
|
||||
"scripts/toggle-file-scan-exclusions.mjs",
|
||||
"scripts/file-exclusions.json",
|
||||
"scripts/zed-settings-sync.mjs",
|
||||
];
|
||||
const OPTIONAL_DIRECTORIES = ["snippets", "themes"];
|
||||
const LOCAL_SYNC_CONFIG = "scripts/settings-sync.json";
|
||||
|
||||
function zedConfigDirectory() {
|
||||
if (process.platform === "win32") {
|
||||
if (!process.env.APPDATA) throw new Error("APPDATA is not set");
|
||||
return join(process.env.APPDATA, "Zed");
|
||||
}
|
||||
return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "zed");
|
||||
}
|
||||
|
||||
function timestamp() {
|
||||
return new Date().toISOString().replace(/[:.]/g, "-");
|
||||
}
|
||||
|
||||
async function writeAtomically(path, contents) {
|
||||
await mkdir(dirname(path), { recursive: true });
|
||||
const temporaryPath = join(dirname(path), `.${basename(path)}.${process.pid}.tmp`);
|
||||
try {
|
||||
await writeFile(temporaryPath, contents, "utf8");
|
||||
await rename(temporaryPath, path);
|
||||
} finally {
|
||||
await rm(temporaryPath, { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function listFilesRecursively(directory, prefix) {
|
||||
if (!existsSync(directory)) return [];
|
||||
const results = [];
|
||||
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
||||
const absolutePath = join(directory, entry.name);
|
||||
const relativePath = `${prefix}/${entry.name}`;
|
||||
if (entry.isDirectory()) results.push(...await listFilesRecursively(absolutePath, relativePath));
|
||||
else if (entry.isFile()) results.push(relativePath);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
async function synchronizedPaths(configDirectory) {
|
||||
const paths = FIXED_FILES.filter((path) => existsSync(join(configDirectory, ...path.split("/"))));
|
||||
for (const directory of OPTIONAL_DIRECTORIES) {
|
||||
paths.push(...await listFilesRecursively(join(configDirectory, directory), directory));
|
||||
}
|
||||
return [...new Set(paths)].sort();
|
||||
}
|
||||
|
||||
function isAllowedBundlePath(path) {
|
||||
if (typeof path !== "string" || path.startsWith("/") || path.includes("\\")) return false;
|
||||
const segments = path.split("/");
|
||||
if (segments.some((segment) => !segment || segment === "." || segment === "..")) return false;
|
||||
if (FIXED_FILES.includes(path) || path === LOCAL_SYNC_CONFIG) return true;
|
||||
return OPTIONAL_DIRECTORIES.some((directory) => path.startsWith(`${directory}/`));
|
||||
}
|
||||
|
||||
function replaceTaskPath(source, path, placeholder) {
|
||||
return source
|
||||
.replaceAll(path.replaceAll("\\", "\\\\"), placeholder)
|
||||
.replaceAll(path.replaceAll("\\", "/"), placeholder);
|
||||
}
|
||||
|
||||
function portableTasks(source, configDirectory) {
|
||||
return replaceTaskPath(
|
||||
replaceTaskPath(
|
||||
source,
|
||||
join(configDirectory, "scripts", "toggle-file-scan-exclusions.mjs"),
|
||||
"__TOGGLE_SCRIPT__",
|
||||
),
|
||||
join(configDirectory, "scripts", "zed-settings-sync.mjs"),
|
||||
"__SYNC_SCRIPT__",
|
||||
);
|
||||
}
|
||||
|
||||
function installedTasks(source, configDirectory) {
|
||||
return source
|
||||
.replaceAll(
|
||||
"__TOGGLE_SCRIPT__",
|
||||
join(configDirectory, "scripts", "toggle-file-scan-exclusions.mjs").replaceAll("\\", "/"),
|
||||
)
|
||||
.replaceAll(
|
||||
"__SYNC_SCRIPT__",
|
||||
join(configDirectory, "scripts", "zed-settings-sync.mjs").replaceAll("\\", "/"),
|
||||
);
|
||||
}
|
||||
|
||||
function hashContents(contents) {
|
||||
return createHash("sha256").update(contents).digest("hex");
|
||||
}
|
||||
|
||||
async function createBundle() {
|
||||
const configDirectory = zedConfigDirectory();
|
||||
const files = {};
|
||||
for (const relativePath of await synchronizedPaths(configDirectory)) {
|
||||
const absolutePath = join(configDirectory, ...relativePath.split("/"));
|
||||
let contents = await readFile(absolutePath, "utf8");
|
||||
if (relativePath === "tasks.json") contents = portableTasks(contents, configDirectory);
|
||||
files[relativePath] = contents;
|
||||
}
|
||||
for (const required of REQUIRED_FILES) {
|
||||
if (!(required in files)) throw new Error(`Cannot export: ${required} is missing`);
|
||||
}
|
||||
const contents = serializeBundle(files);
|
||||
return { files, contents, hash: hashContents(contents) };
|
||||
}
|
||||
|
||||
function serializeBundle(files) {
|
||||
return `${JSON.stringify(files, null, 2)}\n`;
|
||||
}
|
||||
|
||||
function parseBundle(contents, sourceName) {
|
||||
let files;
|
||||
try {
|
||||
files = JSON.parse(contents);
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid JSON in ${sourceName}: ${error.message}`);
|
||||
}
|
||||
if (!files || Array.isArray(files) || typeof files !== "object") {
|
||||
throw new Error(`Invalid Zed settings bundle: ${sourceName}`);
|
||||
}
|
||||
for (const required of REQUIRED_FILES) {
|
||||
if (typeof files[required] !== "string") {
|
||||
throw new Error(`Invalid bundle: ${required} is missing`);
|
||||
}
|
||||
}
|
||||
for (const [path, value] of Object.entries(files)) {
|
||||
if (!isAllowedBundlePath(path) || typeof value !== "string") {
|
||||
throw new Error(`Invalid bundle file: ${path}`);
|
||||
}
|
||||
}
|
||||
return { files, hash: hashContents(contents) };
|
||||
}
|
||||
|
||||
async function exportBundle(path) {
|
||||
const bundle = await createBundle();
|
||||
await writeAtomically(path, bundle.contents);
|
||||
console.log(`Exported Zed settings to ${path}`);
|
||||
console.log(`SHA-256: ${bundle.hash}`);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
async function backupCurrentConfiguration(configDirectory) {
|
||||
const backupDirectory = join(configDirectory, "backups", `sync-import-${timestamp()}`);
|
||||
await mkdir(backupDirectory, { recursive: true });
|
||||
for (const relativePath of await synchronizedPaths(configDirectory)) {
|
||||
const source = join(configDirectory, ...relativePath.split("/"));
|
||||
const destination = join(backupDirectory, ...relativePath.split("/"));
|
||||
await mkdir(dirname(destination), { recursive: true });
|
||||
await copyFile(source, destination);
|
||||
}
|
||||
return backupDirectory;
|
||||
}
|
||||
|
||||
async function installBundleContents(contents, sourceName) {
|
||||
const parsed = parseBundle(contents, sourceName);
|
||||
const configDirectory = zedConfigDirectory();
|
||||
const backupDirectory = await backupCurrentConfiguration(configDirectory);
|
||||
for (const [relativePath, sourceContents] of Object.entries(parsed.files)) {
|
||||
// Gist identity and sync history belong to each machine and are never imported.
|
||||
if (relativePath === LOCAL_SYNC_CONFIG) continue;
|
||||
const destination = join(configDirectory, ...relativePath.split("/"));
|
||||
const installedContents = relativePath === "tasks.json"
|
||||
? installedTasks(sourceContents, configDirectory)
|
||||
: sourceContents;
|
||||
await writeAtomically(destination, installedContents);
|
||||
}
|
||||
console.log(`Imported Zed settings from ${sourceName}`);
|
||||
console.log(`Previous settings were backed up to ${backupDirectory}`);
|
||||
return { ...parsed, backupDirectory };
|
||||
}
|
||||
|
||||
async function importBundle(path) {
|
||||
return installBundleContents(await readFile(path, "utf8"), path);
|
||||
}
|
||||
|
||||
function runPowerShell(command, extraEnv = {}) {
|
||||
const result = spawnSync("powershell.exe", ["-NoProfile", "-STA", "-Command", command], {
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, ...extraEnv },
|
||||
windowsHide: true,
|
||||
});
|
||||
if (result.status !== 0) throw new Error(result.stderr.trim() || "Windows dialog failed");
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
function nativeFileDialog(mode) {
|
||||
if (process.platform === "win32") {
|
||||
const common = "[Console]::OutputEncoding=[Text.Encoding]::UTF8; Add-Type -AssemblyName System.Windows.Forms; ";
|
||||
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)}`;
|
||||
return runPowerShell(command);
|
||||
}
|
||||
if (process.platform === "darwin") {
|
||||
const script = mode === "save"
|
||||
? `POSIX path of (choose file name with prompt "Export Zed settings" default name "${BUNDLE_NAME}")`
|
||||
: "POSIX path of (choose file with prompt \"Import Zed settings\")";
|
||||
const result = spawnSync("osascript", ["-e", script], { encoding: "utf8" });
|
||||
return result.status === 0 ? result.stdout.trim() : "";
|
||||
}
|
||||
const args = ["--file-selection", `--title=${mode === "save" ? "Export" : "Import"} Zed settings`];
|
||||
if (mode === "save") args.push("--save", "--confirm-overwrite", `--filename=${BUNDLE_NAME}`);
|
||||
const result = spawnSync("zenity", args, { encoding: "utf8" });
|
||||
return result.status === 0 ? result.stdout.trim() : "";
|
||||
}
|
||||
|
||||
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))";
|
||||
return runPowerShell(command, {
|
||||
ZED_SYNC_PROMPT: prompt,
|
||||
ZED_SYNC_TITLE: title,
|
||||
ZED_SYNC_DEFAULT: defaultValue,
|
||||
});
|
||||
}
|
||||
if (process.platform === "darwin") {
|
||||
const escaped = (value) => value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
||||
const script = `text returned of (display dialog "${escaped(prompt)}" with title "${escaped(title)}" default answer "${escaped(defaultValue)}")`;
|
||||
const result = spawnSync("osascript", ["-e", script], { encoding: "utf8" });
|
||||
return result.status === 0 ? result.stdout.trim() : "";
|
||||
}
|
||||
const result = spawnSync("zenity", ["--entry", `--title=${title}`, `--text=${prompt}`, `--entry-text=${defaultValue}`], { encoding: "utf8" });
|
||||
return result.status === 0 ? result.stdout.trim() : "";
|
||||
}
|
||||
|
||||
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)";
|
||||
runPowerShell(command, {
|
||||
ZED_SYNC_MESSAGE: message,
|
||||
ZED_SYNC_TITLE: title,
|
||||
ZED_SYNC_ICON: error ? "Error" : "Information",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (process.platform === "darwin") {
|
||||
const escaped = message.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
||||
spawnSync("osascript", ["-e", `display dialog "${escaped}" with title "${title}" buttons {"OK"}`]);
|
||||
return;
|
||||
}
|
||||
spawnSync("zenity", [error ? "--error" : "--info", `--title=${title}`, `--text=${message}`]);
|
||||
}
|
||||
|
||||
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'))";
|
||||
return runPowerShell(command, { ZED_SYNC_MESSAGE: message, ZED_SYNC_TITLE: title }) === "Yes";
|
||||
}
|
||||
if (process.platform === "darwin") {
|
||||
const escaped = message.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
||||
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" });
|
||||
return result.status === 0 && result.stdout.trim() === "Continue";
|
||||
}
|
||||
return spawnSync("zenity", ["--question", `--title=${title}`, `--text=${message}`]).status === 0;
|
||||
}
|
||||
|
||||
async function readSyncConfiguration() {
|
||||
const path = join(zedConfigDirectory(), ...LOCAL_SYNC_CONFIG.split("/"));
|
||||
let source = {};
|
||||
try {
|
||||
source = JSON.parse(await readFile(path, "utf8"));
|
||||
} catch (error) {
|
||||
if (error.code !== "ENOENT") throw error;
|
||||
}
|
||||
return {
|
||||
path,
|
||||
config: {
|
||||
gist_id: typeof source.gist_id === "string" ? source.gist_id : "",
|
||||
last_synced_hash: typeof source.last_synced_hash === "string" ? source.last_synced_hash : "",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function saveSyncConfiguration(path, config) {
|
||||
await writeAtomically(path, `${JSON.stringify(config, null, 2)}\n`);
|
||||
}
|
||||
|
||||
function normalizeGistId(value) {
|
||||
const matches = value.trim().match(/[a-f0-9]{8,}/gi);
|
||||
if (!matches?.length) throw new Error("Enter a valid GitHub Gist ID or URL");
|
||||
return matches.at(-1).toLowerCase();
|
||||
}
|
||||
|
||||
async function promptForGist() {
|
||||
const state = await readSyncConfiguration();
|
||||
const entered = nativeInput(
|
||||
"Paste the GitHub Gist ID or full Gist URL. The previous value is remembered on this machine.",
|
||||
"Zed Settings Gist",
|
||||
state.config.gist_id,
|
||||
);
|
||||
if (!entered) return null;
|
||||
const gistId = normalizeGistId(entered);
|
||||
if (gistId !== state.config.gist_id) {
|
||||
state.config.gist_id = gistId;
|
||||
state.config.last_synced_hash = "";
|
||||
await saveSyncConfiguration(state.path, state.config);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function runGh(args, input) {
|
||||
const result = spawnSync("gh", args, { encoding: "utf8", input, windowsHide: true });
|
||||
if (result.error?.code === "ENOENT") {
|
||||
throw new Error("GitHub CLI (gh) is not installed. Install it, then run: gh auth login");
|
||||
}
|
||||
if (result.status !== 0) throw new Error(result.stderr.trim() || `gh ${args[0]} failed`);
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
function readRemoteGist(gistId) {
|
||||
const gist = JSON.parse(runGh(["api", `gists/${gistId}`]));
|
||||
const file = gist.files?.[BUNDLE_NAME];
|
||||
if (!file) return { gist, parsed: null, contents: null };
|
||||
if (file.truncated) throw new Error(`${BUNDLE_NAME} is too large to read through the Gist API`);
|
||||
return { gist, parsed: parseBundle(file.content, `Gist ${gistId}`), contents: file.content };
|
||||
}
|
||||
|
||||
function updateRemoteGist(gistId, contents) {
|
||||
const payload = JSON.stringify({ files: { [BUNDLE_NAME]: { content: contents } } });
|
||||
return JSON.parse(runGh(["api", "--method", "PATCH", `gists/${gistId}`, "--input", "-"], payload));
|
||||
}
|
||||
|
||||
function shortHash(hash) {
|
||||
return hash ? hash.slice(0, 12) : "none";
|
||||
}
|
||||
|
||||
async function configureGist() {
|
||||
const state = await promptForGist();
|
||||
if (!state) return;
|
||||
nativeMessage(`Saved Gist ID:\n${state.config.gist_id}\n\nAuthentication is managed by GitHub CLI (gh).`);
|
||||
}
|
||||
|
||||
async function pushGist() {
|
||||
const state = await promptForGist();
|
||||
if (!state) return;
|
||||
const localBundle = await createBundle();
|
||||
const remote = readRemoteGist(state.config.gist_id);
|
||||
const remoteHash = remote.parsed?.hash || "";
|
||||
const lastHash = state.config.last_synced_hash;
|
||||
if (remoteHash && remoteHash !== localBundle.hash) {
|
||||
const remoteChanged = !lastHash || remoteHash !== lastHash;
|
||||
if (remoteChanged) {
|
||||
const proceed = nativeConfirm(
|
||||
`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?`,
|
||||
);
|
||||
if (!proceed) return;
|
||||
}
|
||||
}
|
||||
const updated = updateRemoteGist(state.config.gist_id, localBundle.contents);
|
||||
state.config.last_synced_hash = localBundle.hash;
|
||||
await saveSyncConfiguration(state.path, state.config);
|
||||
nativeMessage(`Uploaded to Gist ${state.config.gist_id}\n\nSHA-256: ${localBundle.hash}\nUpdated: ${updated.updated_at || "unknown"}`);
|
||||
}
|
||||
|
||||
async function pullGist() {
|
||||
const state = await promptForGist();
|
||||
if (!state) return;
|
||||
const localBundle = await createBundle();
|
||||
const remote = readRemoteGist(state.config.gist_id);
|
||||
if (!remote.parsed) throw new Error(`Gist ${state.config.gist_id} does not contain ${BUNDLE_NAME}`);
|
||||
if (localBundle.hash !== remote.parsed.hash) {
|
||||
const localChanged = !state.config.last_synced_hash || localBundle.hash !== state.config.last_synced_hash;
|
||||
if (localChanged) {
|
||||
const proceed = nativeConfirm(
|
||||
`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.`,
|
||||
);
|
||||
if (!proceed) return;
|
||||
}
|
||||
await installBundleContents(remote.contents, `Gist ${state.config.gist_id}`);
|
||||
}
|
||||
state.config.last_synced_hash = remote.parsed.hash;
|
||||
await saveSyncConfiguration(state.path, state.config);
|
||||
nativeMessage(`Downloaded from Gist ${state.config.gist_id}\n\nSHA-256: ${remote.parsed.hash}\nRemote updated: ${remote.gist.updated_at || "unknown"}`);
|
||||
}
|
||||
|
||||
async function showGistStatus() {
|
||||
const state = await promptForGist();
|
||||
if (!state) return;
|
||||
const localBundle = await createBundle();
|
||||
const remote = readRemoteGist(state.config.gist_id);
|
||||
const remoteHash = remote.parsed?.hash || "";
|
||||
const lastHash = state.config.last_synced_hash;
|
||||
let status = "The Gist does not contain a Zed settings file.";
|
||||
if (remoteHash === localBundle.hash) status = "Synchronized";
|
||||
else if (!lastHash) status = "Not synchronized on this machine";
|
||||
else if (localBundle.hash === lastHash) status = "Remote changes available";
|
||||
else if (remoteHash === lastHash) status = "Local changes pending upload";
|
||||
else status = "Conflict: local and remote settings both changed";
|
||||
nativeMessage(
|
||||
`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"}`,
|
||||
"Zed Settings Gist Status",
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [mode, explicitPath] = process.argv.slice(2);
|
||||
if (mode === "export") return exportBundle(resolve(explicitPath));
|
||||
if (mode === "import") return importBundle(resolve(explicitPath));
|
||||
if (mode === "export-dialog") {
|
||||
const path = nativeFileDialog("save");
|
||||
if (path) await exportBundle(path);
|
||||
return;
|
||||
}
|
||||
if (mode === "import-dialog") {
|
||||
const path = nativeFileDialog("open");
|
||||
if (path) await importBundle(path);
|
||||
return;
|
||||
}
|
||||
if (mode === "gist-configure") return configureGist();
|
||||
if (mode === "gist-status") return showGistStatus();
|
||||
if (mode === "gist-push") return pushGist();
|
||||
if (mode === "gist-pull") return pullGist();
|
||||
throw new Error(`Unknown mode: ${mode || "<missing>"}`);
|
||||
}
|
||||
|
||||
try {
|
||||
await main();
|
||||
} catch (error) {
|
||||
console.error(error.stack || error.message);
|
||||
try {
|
||||
nativeMessage(error.message, "Zed Settings Sync Error", true);
|
||||
} catch {
|
||||
// The terminal output remains available when the native dialog backend is unavailable.
|
||||
}
|
||||
process.exitCode = 1;
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
// Zed settings
|
||||
//
|
||||
// For information on how to configure Zed, see the Zed
|
||||
// documentation: https://zed.dev/docs/configuring-zed
|
||||
//
|
||||
// To see all of Zed's default settings without changing your
|
||||
// custom settings, run `zed: open default settings` from the
|
||||
// command palette (cmd-shift-p / ctrl-shift-p)
|
||||
{
|
||||
"cli_default_open_behavior": "existing_window",
|
||||
"icon_theme": "Material Icon Theme",
|
||||
"auto_install_extensions": {
|
||||
"dockerfile": true,
|
||||
"env": true,
|
||||
"html": true,
|
||||
"material-icon-theme": true,
|
||||
"mcp-server-context7": true,
|
||||
"mcp-server-github": true,
|
||||
"one-dark-pro": true,
|
||||
"powershell": true,
|
||||
"toml": true
|
||||
},
|
||||
"disable_ai": false,
|
||||
"proxy": "",
|
||||
"agent": {
|
||||
"sidebar_side": "right",
|
||||
"favorite_models": [],
|
||||
"model_parameters": []
|
||||
},
|
||||
"project_panel": {
|
||||
"dock": "left"
|
||||
},
|
||||
"telemetry": {
|
||||
"diagnostics": false,
|
||||
"metrics": false,
|
||||
"anthropic_retention": false
|
||||
},
|
||||
"terminal": {
|
||||
"font_size": 16.0,
|
||||
"cursor_shape": "bar"
|
||||
},
|
||||
"base_keymap": "VSCode",
|
||||
"file_scan_exclusions": [
|
||||
"**/.git",
|
||||
"**/.svn",
|
||||
"**/.hg",
|
||||
"**/.jj",
|
||||
"**/CVS",
|
||||
"**/.DS_Store",
|
||||
"**/Thumbs.db",
|
||||
"**/.classpath",
|
||||
"**/.settings",
|
||||
"**/.playwright-cli",
|
||||
"**/output",
|
||||
"**/.cursor",
|
||||
"**/.vscode",
|
||||
"**/.idea",
|
||||
"**/.fleet",
|
||||
"**/node_modules",
|
||||
"**/bun.lockb",
|
||||
"**/pnpm-lock.yaml",
|
||||
"**/package-lock.json",
|
||||
"**/yarn.lock",
|
||||
"**/dist",
|
||||
"**/out",
|
||||
"**/build",
|
||||
"**/.next",
|
||||
"**/.nuxt",
|
||||
"**/.output",
|
||||
"**/.cache",
|
||||
"**/.turbo",
|
||||
"**/.vercel",
|
||||
"**/.netlify",
|
||||
"**/tsconfig.tsbuildinfo",
|
||||
"**/auto-imports.d.ts",
|
||||
"**/components.json",
|
||||
"**/unplugin-vue-components.d.ts",
|
||||
"src/*.d.ts",
|
||||
"**/__pycache__",
|
||||
"**/__init__.py",
|
||||
"**/.ipynb_checkpoints",
|
||||
"**/.pytest_cache",
|
||||
"**/.mypy_cache",
|
||||
"**/.venv",
|
||||
"**/env",
|
||||
"**/venv",
|
||||
"**/.github",
|
||||
"**/.husky",
|
||||
"**/.npmrc",
|
||||
"**/.eslintrc*",
|
||||
"**/.prettier*",
|
||||
"**/.editorconfig",
|
||||
"**/.dockerignore",
|
||||
"**/docker-compose.override.yml",
|
||||
"**/cypress*",
|
||||
"**/prisma/migrations"
|
||||
],
|
||||
"preview_tabs": {
|
||||
"enabled": false
|
||||
},
|
||||
"git_panel": {
|
||||
"fallback_branch_name": "master"
|
||||
},
|
||||
"sticky_scroll": {
|
||||
"enabled": true
|
||||
},
|
||||
"minimap": {
|
||||
"show": "always"
|
||||
},
|
||||
"hover_popover_delay": 250,
|
||||
"cursor_blink": true,
|
||||
"autosave": "on_focus_change",
|
||||
"buffer_font_fallbacks": [
|
||||
"Consolas",
|
||||
"Sarasa Gothic SC",
|
||||
"Courier New",
|
||||
"monospace"
|
||||
],
|
||||
"buffer_font_family": "Fira Code",
|
||||
"auto_indent_on_paste": true,
|
||||
"linked_edits": true,
|
||||
"show_edit_predictions": true,
|
||||
"line_ending": "prefer_lf",
|
||||
"hard_tabs": false,
|
||||
"tab_size": 2,
|
||||
"agent_servers": {
|
||||
"codex-acp": {
|
||||
"type": "registry"
|
||||
}
|
||||
},
|
||||
"ui_font_size": 16,
|
||||
"buffer_font_size": 16.0,
|
||||
"theme": {
|
||||
"mode": "system",
|
||||
"light": "One Light",
|
||||
"dark": "One Dark Pro",
|
||||
},
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
// Project tasks configuration. See https://zed.dev/docs/tasks for documentation.
|
||||
//
|
||||
// Example:
|
||||
[
|
||||
/* The generated example task is intentionally disabled.
|
||||
{
|
||||
"label": "Example task",
|
||||
"command": "for i in {1..5}; do echo \"Hello $i/5\"; sleep 1; done",
|
||||
//"args": [],
|
||||
// Env overrides for the command, will be appended to the terminal's environment from the settings.
|
||||
"env": { "foo": "bar" },
|
||||
// Current working directory to spawn the command into, defaults to current project root.
|
||||
//"cwd": "/path/to/working/directory",
|
||||
// Whether to use a new terminal tab or reuse the existing one to spawn the process, defaults to `false`.
|
||||
"use_new_terminal": false,
|
||||
// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish, defaults to `false`.
|
||||
"allow_concurrent_runs": false,
|
||||
// What to do with the terminal pane and tab, after the command was started:
|
||||
// * `always` — always show the task's pane, and focus the corresponding tab in it (default)
|
||||
// * `no_focus` — always show the task's pane, add the task's tab in it, but don't focus it
|
||||
// * `never` — do not alter focus, but still add/reuse the task's tab in its pane
|
||||
"reveal": "always",
|
||||
// Where to place the task's terminal item after starting the task:
|
||||
// * `dock` — in the terminal dock, "regular" terminal items' place (default)
|
||||
// * `center` — in the central pane group, "main" editor area
|
||||
"reveal_target": "dock",
|
||||
// What to do with the terminal pane and tab, after the command had finished:
|
||||
// * `never` — Do nothing when the command finishes (default)
|
||||
// * `always` — always hide the terminal tab, hide the pane also if it was the last tab in it
|
||||
// * `on_success` — hide the terminal tab on task success only, otherwise behaves similar to `always`
|
||||
"hide": "never",
|
||||
// Which shell to use when running a task inside the terminal.
|
||||
// May take 3 values:
|
||||
// 1. (default) Use the system's default terminal configuration in /etc/passwd
|
||||
// "shell": "system"
|
||||
// 2. A program:
|
||||
// "shell": {
|
||||
// "program": "sh"
|
||||
// }
|
||||
// 3. A program with arguments:
|
||||
// "shell": {
|
||||
// "with_arguments": {
|
||||
// "program": "/bin/bash",
|
||||
// "args": ["--login"]
|
||||
// }
|
||||
// }
|
||||
"shell": "system",
|
||||
// Whether to show the task line in the output of the spawned task, defaults to `true`.
|
||||
"show_summary": true,
|
||||
// Whether to show the command line in the output of the spawned task, defaults to `true`.
|
||||
"show_command": true,
|
||||
// Which edited buffers to save before running the task:
|
||||
// * `all` — save all edited buffers
|
||||
// * `current` — save currently active buffer only
|
||||
// * `none` — don't save any buffers
|
||||
"save": "none",
|
||||
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
|
||||
// "tags": []
|
||||
}, */
|
||||
{
|
||||
"label": "Toggle excluded files",
|
||||
"command": "node",
|
||||
"args": [
|
||||
"__TOGGLE_SCRIPT__"
|
||||
],
|
||||
"use_new_terminal": false,
|
||||
"allow_concurrent_runs": false,
|
||||
"reveal": "never",
|
||||
"hide": "always",
|
||||
"show_summary": false,
|
||||
"show_command": false,
|
||||
"save": "none"
|
||||
},
|
||||
{
|
||||
"label": "Zed Settings: Export JSON...",
|
||||
"command": "node",
|
||||
"args": [
|
||||
"__SYNC_SCRIPT__",
|
||||
"export-dialog"
|
||||
],
|
||||
"allow_concurrent_runs": false,
|
||||
"reveal": "never",
|
||||
"hide": "always",
|
||||
"show_summary": false,
|
||||
"show_command": false,
|
||||
"save": "all"
|
||||
},
|
||||
{
|
||||
"label": "Zed Settings: Import JSON...",
|
||||
"command": "node",
|
||||
"args": [
|
||||
"__SYNC_SCRIPT__",
|
||||
"import-dialog"
|
||||
],
|
||||
"allow_concurrent_runs": false,
|
||||
"reveal": "never",
|
||||
"hide": "always",
|
||||
"show_summary": false,
|
||||
"show_command": false,
|
||||
"save": "all"
|
||||
},
|
||||
{
|
||||
"label": "Zed Settings: Configure GitHub Gist...",
|
||||
"command": "node",
|
||||
"args": [
|
||||
"__SYNC_SCRIPT__",
|
||||
"gist-configure"
|
||||
],
|
||||
"allow_concurrent_runs": false,
|
||||
"reveal": "never",
|
||||
"hide": "always",
|
||||
"show_summary": false,
|
||||
"show_command": false,
|
||||
"save": "all"
|
||||
},
|
||||
{
|
||||
"label": "Zed Settings: Show GitHub Gist Status",
|
||||
"command": "node",
|
||||
"args": [
|
||||
"__SYNC_SCRIPT__",
|
||||
"gist-status"
|
||||
],
|
||||
"allow_concurrent_runs": false,
|
||||
"reveal": "never",
|
||||
"hide": "always",
|
||||
"show_summary": false,
|
||||
"show_command": false,
|
||||
"save": "all"
|
||||
},
|
||||
{
|
||||
"label": "Zed Settings: Push to GitHub Gist",
|
||||
"command": "node",
|
||||
"args": [
|
||||
"__SYNC_SCRIPT__",
|
||||
"gist-push"
|
||||
],
|
||||
"allow_concurrent_runs": false,
|
||||
"reveal": "no_focus",
|
||||
"hide": "on_success",
|
||||
"show_summary": false,
|
||||
"show_command": false,
|
||||
"save": "all"
|
||||
},
|
||||
{
|
||||
"label": "Zed Settings: Pull from GitHub Gist",
|
||||
"command": "node",
|
||||
"args": [
|
||||
"__SYNC_SCRIPT__",
|
||||
"gist-pull"
|
||||
],
|
||||
"allow_concurrent_runs": false,
|
||||
"reveal": "no_focus",
|
||||
"hide": "on_success",
|
||||
"show_summary": false,
|
||||
"show_command": false,
|
||||
"save": "all"
|
||||
},
|
||||
{
|
||||
"label": "Zed Settings: Authenticate GitHub...",
|
||||
"command": "gh",
|
||||
"args": ["auth", "login"],
|
||||
"allow_concurrent_runs": false,
|
||||
"reveal": "always",
|
||||
"hide": "never",
|
||||
"show_summary": false,
|
||||
"show_command": true,
|
||||
"save": "none"
|
||||
},
|
||||
]
|
||||
Reference in New Issue
Block a user