文件预览

syncthing.md

查看 Dotfile 技能包中的文件内容。

文件内容

syncthing.md

# Syncthing Integration

Syncs chezmoi source directory via Syncthing to share dotfiles across multiple machines.

## Register Folder via API

Auto-register chezmoi folder using Syncthing REST API:

```bash
# API key (macOS - uses xmllint, grep -oP is GNU-only and doesn't work on macOS)
API_KEY=$(xmllint --xpath '//configuration/gui/apikey/text()' ~/Library/Application\ Support/Syncthing/config.xml)

# Add folder
curl -X POST -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  http://localhost:8384/rest/config/folders \
  -d '{
    "id": ".local/share/chezmoi",
    "label": ".local/share/chezmoi",
    "path": "~/.local/share/chezmoi",
    "type": "sendreceive",
    "versioning": {
      "type": "staggered",
      "params": {"maxAge": "31536000"}
    }
  }'
```

## Auto Ignore Setup

Creates `.stignore` during chezmoi initialization:

```bash
# ~/.local/share/chezmoi/.stignore
.git
(?d).DS_Store
*.bak
```

### `(?d)` Prefix Guide

When a parent directory is deleted on a remote, deletion is blocked if ignored files remain locally:

```
syncing: delete dir: directory has been deleted on a remote device
but contains ignored files (see ignore documentation for (?d) prefix)
```

**`(?d)` prefix**: Allows parent directory deletion even if ignored files exist.

### `(?d)` Application Rules

| Apply | Pattern | Reason |
|-------|---------|--------|
| **Prohibited** | `.bak` | Backup files -- must be preserved locally even on remote deletion |
| **Prohibited** | `.git` | Repository data -- must be preserved locally |
| **Prohibited** | `.env*` | Environment variables/secrets -- must be preserved locally |
| Apply | `.DS_Store` | Volatile metadata |
| Apply | `.ansible`, `.terraform`, `.venv`, `venv`, `node_modules` | Reinstallable runtime/dependencies |
| Apply | `build`, `cache`, `defined`, `dist`, `out`, `target` | Regenerable build artifacts |

### `.claude/.stignore` Current Patterns

```bash
# Backups/secrets -- (?d) prohibited, preserve locally on remote deletion
.bak
.env*
.git
# Regenerable -- (?d) applied, cleaned up together on remote deletion
(?d).ansible
(?d).DS_Store
(?d).terraform
(?d).venv
(?d)build
(?d)cache
(?d)defined
(?d)dist
(?d)node_modules
(?d)out
(?d)target
(?d)venv
# Whitelist
!commands
!hooks
!plugins/marketplaces
!projects
!scripts
*
```

> **Strictly prohibited**: Adding `(?d)` to `.bak`, `.git`, `.env*` -- backup/secret loss on remote deletion

## Directory Structure

```
~/.local/share/chezmoi/
├── .chezmoitemplates/     # Shared data (JSON, etc.)
│   └── mcp-servers.json
├── .chezmoi-lib/          # Shared scripts (executables)
│   ├── executable_*.sh
│   └── ...
├── .stignore              # Syncthing ignore patterns
└── ...
```

## New Machine Setup

```bash
# 1. After syncing chezmoi source via Syncthing
# 2. Initialize chezmoi (keep source directory)
chezmoi init --source ~/.local/share/chezmoi

# 3. Apply
chezmoi apply
```

## Managing Syncthing Default Config with chezmoi

Manage Syncthing defaults via chezmoi modify:

```
~/.local/share/chezmoi/private_Library/private_Application Support/private_Syncthing/
└── modify_private_config.xml.tmpl
```

**Managed items:**
- `defaults/folder`: minDiskFree 1GB, versioning staggered 1 year
- `defaults/ignores`: Global ignore patterns

**Behavior:** On chezmoi apply, settings are applied via Syncthing API -> config.xml auto-updated

## Sync Diagnostics

### Get API Key

**macOS / Linux:**
```bash
API_KEY=$(xmllint --xpath '//configuration/gui/apikey/text()' ~/Library/Application\ Support/Syncthing/config.xml)
```

**Windows (PowerShell):**
```powershell
$configPath = "$env:LOCALAPPDATA\Syncthing\config.xml"
[xml]$xml = Get-Content $configPath
$API_KEY = $xml.configuration.gui.apikey
```

### Check Folder Status

```bash
# Full folder sync status (state, globalFiles, localFiles, needFiles)
curl -s -H "X-API-Key: $API_KEY" "http://localhost:8384/rest/db/status?folder=<FOLDER_ID>"
```

| Field | Meaning |
|-------|---------|
| `state` | `idle` normal, `scanning` scanning, `sync-waiting` waiting to sync |
| `globalFiles` | Total file count across all devices |
| `localFiles` | File count on this device |
| `needFiles` | Files still to be received |

**global > local && needFiles=0**: Files only on other devices (`.stignore` whitelist differences). Normal.

### Check Incomplete Items

```bash
# List of files not yet synced
curl -s -H "X-API-Key: $API_KEY" "http://localhost:8384/rest/db/need?folder=<FOLDER_ID>"

# Completion from a specific device to this device
curl -s -H "X-API-Key: $API_KEY" "http://localhost:8384/rest/db/completion?folder=<FOLDER_ID>&device=<DEVICE_ID>"
```

### Connection Status

```bash
# Check connected/disconnected devices
curl -s -H "X-API-Key: $API_KEY" http://localhost:8384/rest/system/connections

# Check device names
curl -s -H "X-API-Key: $API_KEY" http://localhost:8384/rest/config/devices
```

### Rescan (Index Refresh)

```bash
# Rescan specific folder -- effective for resolving stale states like sync-waiting
curl -s -X POST -H "X-API-Key: $API_KEY" "http://localhost:8384/rest/db/scan?folder=<FOLDER_ID>"
```

### DB Reset (Full Index Reset)

Used when stale entries from offline devices remain or when encountering persistent transfer queue freezes. Settings/keys are preserved; only the index is rebuilt.

> **Note**: `syncthing --reset-database` was removed in v2.0. Replaced by deleting the `index-v2` directory.

**macOS / Linux:**
```bash
# 1. Stop Syncthing
brew services stop syncthing

# 2. Back up and delete index
mv ~/Library/Application\ Support/Syncthing/index-v2 \
   ~/Library/Application\ Support/Syncthing/index-v2.bak

# 3. Restart (index auto-rebuilt)
brew services start syncthing

# 4. Remove backup after verification
rm -rf ~/Library/Application\ Support/Syncthing/index-v2.bak
```

**Windows (PowerShell - Run as Admin if service-managed):**
```powershell
# 1. Stop Syncthing service
Stop-Service syncthing
# Force kill processes if locked
Get-Process | Where-Object { $_.Name -like "*syncthing*" } | Stop-Process -Force

# 2. Back up and delete index
$dbPath = "$env:LOCALAPPDATA\Syncthing\index-v2"
$backupPath = "$env:LOCALAPPDATA\Syncthing\index-v2.bak"
if (Test-Path $backupPath) { Remove-Item $backupPath -Recurse -Force }
Move-Item $dbPath $backupPath -Force

# 3. Start service (index auto-rebuilt)
Start-Service syncthing

# 4. Verify and clean up backup later
Remove-Item $backupPath -Recurse -Force
```

**Check index path**: `syncthing paths` or `syncthing.exe paths` -> "Database location" entry

### Windows Service Account: User vs LocalSystem (HARD STOP)

**Recommended**: Run Syncthing as a Windows service under the **user account**, not `LocalSystem`. User-account service can expand `~` paths in `config.xml` and avoids the `.git/` corruption pattern seen with `LocalSystem`.

| Aspect | `LocalSystem` (avoid) | User account (recommended) |
|--------|----------------------|---------------------------|
| `~` path expansion | ❌ Fails — needs absolute path conversion | ✅ Native `~` resolution |
| User profile access | ❌ Different `$HOME` (`C:\Windows\System32\config\systemprofile`) | ✅ Matches `C:\Users\<user>` |
| `.git/` corruption | ⚠️ Higher risk (no user lock context) | Lower risk |
| Survives reboot | ✅ Yes | ✅ Yes (with `Automatic` start type) |
| Requires user login | ❌ No | ❌ No (service can run without login) |

#### Migrate LocalSystem → User Account (Task Scheduler with hidden VBS launcher)

**Recommended approach**: Task Scheduler at user logon + VBS launcher for hidden execution. No password storage required. Run via `gsudo` (see `~/.agents/rules/windows.md` "Admin command = gsudo default").

##### Step 1: Create VBS hidden launcher

`syncthing.exe` is a console app — running it directly from Task Scheduler shows a console window that, **when closed, terminates the process**. Wrap it in VBS to launch hidden.

```vbs
' ~/.local/bin/syncthing-hidden.vbs
Set objShell = CreateObject("WScript.Shell")
objShell.Run """C:\ProgramData\chocolatey\bin\syncthing.exe"" --no-browser --home=""C:\Users\<USERNAME>\AppData\Local\Syncthing""", 0, False
```

The `0` argument = `vbHidden` (no window). The `False` = don't wait for completion (VBS exits, child syncthing.exe keeps running).

##### Step 2: Migration script

`~/.local/share/syncthing-migrate-to-user.ps1`:

```powershell
# Stop and delete existing LocalSystem service
Stop-Service syncthing -Force -ErrorAction SilentlyContinue
sc.exe delete syncthing

# Register Task Scheduler entry
$user = "$env:USERDOMAIN\$env:USERNAME"
$vbs = "$env:USERPROFILE\.local\bin\syncthing-hidden.vbs"

$action = New-ScheduledTaskAction `
    -Execute "wscript.exe" `
    -Argument "`"$vbs`"" `
    -WorkingDirectory $env:USERPROFILE
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $user
$principal = New-ScheduledTaskPrincipal `
    -UserId $user `
    -LogonType Interactive `
    -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet `
    -AllowStartIfOnBatteries `
    -DontStopIfGoingOnBatteries `
    -StartWhenAvailable `
    -ExecutionTimeLimit (New-TimeSpan -Days 0) `
    -RestartCount 3 `
    -RestartInterval (New-TimeSpan -Minutes 1) `
    -Hidden

Register-ScheduledTask `
    -TaskName "Syncthing" `
    -Action $action `
    -Trigger $trigger `
    -Principal $principal `
    -Settings $settings
Start-ScheduledTask -TaskName "Syncthing"
```

##### Step 3: Run via gsudo

```powershell
gsudo powershell -NoProfile -ExecutionPolicy Bypass -File "$env:USERPROFILE\.local\share\syncthing-migrate-to-user.ps1"
```

##### Step 4: Verify

```powershell
Get-Process syncthing | ForEach-Object {
    $owner = (Get-WmiObject Win32_Process -Filter "ProcessId=$($_.Id)").GetOwner()
    $hidden = if ([string]::IsNullOrEmpty($_.MainWindowTitle)) { "hidden" } else { "visible" }
    Write-Host "PID $($_.Id) owner=$($owner.Domain)\$($owner.User) $hidden"
}
# Expected: owner=<DOMAIN>\<USER>, hidden
```

#### Alternative: Windows Service (user account) — requires password

If you need Syncthing to start before user logon, register as a Windows Service under the user account. Service Control Manager will encrypt and store the password. This trade-off (password storage) is rarely worth it on personal machines — prefer Task Scheduler unless multi-user / server context.

```powershell
# Run as Admin (or via gsudo)
sc.exe create syncthing binPath= "`"$shawl`" run --name syncthing -- `"$syncthing`" --no-browser --home=$home" start= auto obj= $user password= "<USER_PASSWORD>"
# Plus: grant "Log on as a service" right via secpol.msc or ntrights.exe
```

#### After Migration: Use `~` Paths

Once running under user account, `config.xml` folder paths can use `~`:

```xml
<folder id="..." path="~/.claude" ... />
```

Syncthing will resolve `~` to `C:\Users\<USERNAME>` natively.

### Troubleshooting: Garbage `encryptionPassword` (Antigravity / Gemini re-registration)

**Symptom**: All folders report `Failed to verify encryption consistency` against connected devices, message:
`remote expects to exchange plain data, but local data is encrypted (folder-type receive-encrypted)`.

Folder type is `sendreceive` on both ends, yet Syncthing treats one side as `receive-encrypted`.

**Cause**: When Antigravity (Gemini) re-registers Syncthing folders during config sync, it writes non-empty whitespace strings (commonly `&#xA;      ` — LF + 6 spaces) into every `<encryptionPassword>` element under each `<folder>/<device>` block and the `<defaults>/<folder>/<device>` template. Syncthing treats any non-empty string as a valid password and switches the folder into encryption mode, which then mismatches the remote's `sendreceive` configuration.

The corrupted password is **invisible to the Web UI password field** (renders as empty) and to PowerShell `[xml]` access (auto-trimmed) — diagnosis requires reading the raw XML.

#### Diagnosis (raw XML hex inspection)

```powershell
$cfg = "$env:LOCALAPPDATA\Syncthing\config.xml"
$content = Get-Content $cfg -Raw
$pattern = '<encryptionPassword>([^<]*)</encryptionPassword>'
$mtchs = [regex]::Matches($content, $pattern)
$empty = 0; $nonempty = 0
foreach ($m in $mtchs) {
    $pwd = $m.Groups[1].Value
    if ($pwd.Length -eq 0) { $empty++ } else {
        $nonempty++
        $hex = ($pwd.ToCharArray() | ForEach-Object { "{0:X2}" -f [int]$_ }) -join " "
        Write-Host "  hex='$hex' (len=$($pwd.Length))"
    }
}
Write-Host "empty=$empty, non-empty=$nonempty"
```

A typical garbage value `hex='26 23 78 41 3B 20 20 20 20 20 20'` decodes to `&#xA;      ` (LF + 6 spaces). If `non-empty > 0`, the bug is present.

#### Fix (API PUT — clears folders + defaults)

```powershell
$cfg = "$env:LOCALAPPDATA\Syncthing\config.xml"
[xml]$xml = Get-Content $cfg
$API_KEY = $xml.configuration.gui.apikey

# 1. Clear all folder/device encryption passwords
$folders = Invoke-RestMethod -Uri "http://localhost:8384/rest/config/folders" -Headers @{"X-API-Key"=$API_KEY}
foreach ($f in $folders) {
    foreach ($d in $f.devices) {
        if ($d.encryptionPassword -and $d.encryptionPassword.Length -gt 0) { $d.encryptionPassword = "" }
    }
}
$body = $folders | ConvertTo-Json -Depth 10 -Compress
Invoke-RestMethod -Method Put -Uri "http://localhost:8384/rest/config/folders" `
    -Headers @{"X-API-Key"=$API_KEY; "Content-Type"="application/json"} -Body $body | Out-Null

# 2. Clear defaults/folder template (prevents recurrence on new folders)
$defaults = Invoke-RestMethod -Uri "http://localhost:8384/rest/config/defaults/folder" -Headers @{"X-API-Key"=$API_KEY}
foreach ($d in $defaults.devices) {
    if ($d.encryptionPassword -and $d.encryptionPassword.Length -gt 0) { $d.encryptionPassword = "" }
}
$dbody = $defaults | ConvertTo-Json -Depth 10 -Compress
Invoke-RestMethod -Method Put -Uri "http://localhost:8384/rest/config/defaults/folder" `
    -Headers @{"X-API-Key"=$API_KEY; "Content-Type"="application/json"} -Body $dbody | Out-Null
```

No Syncthing restart required — the API PUT writes through to config.xml and the running daemon picks up the change. Encryption errors stop within seconds.

#### Don't / Do

| # | Don't | Do |
|---|-------|-----|
| 1 | Trust the Web UI password field appearing empty | Read raw config.xml + hex-dump every `<encryptionPassword>` |
| 2 | Restart / unpause / re-add folders to fix the symptom | The garbage password persists across all those operations — clear via API PUT |
| 3 | Clear only folders, skip `<defaults>` | New folders will inherit the garbage password again — clear both |
| 4 | Use PowerShell `[xml]` parsing to compare passwords | `[xml]` auto-trims whitespace, hiding the garbage. Use regex on raw text |
| 5 | Assume "remote device has wrong type" and ask the user to reconfigure the peer | The peer is correct; the local non-empty password is what flips local-side to encrypted mode |

#### Verification

After the fix:
```powershell
$log = Invoke-RestMethod -Uri "http://localhost:8384/rest/system/log" -Headers @{"X-API-Key"=$API_KEY}
$recent = $log.messages | Where-Object {
    ($_.message -match "encryption") -and ([DateTime]$_.when -gt (Get-Date).AddMinutes(-2))
}
Write-Host "Encryption errors in last 2min: $($recent.Count)"  # expected: 0
```

### Legacy: "Folder Path Missing" Workaround (LocalSystem only)

**Use only if migrating to user account is not feasible.** Converts `~` to absolute paths in `config.xml`:

```powershell
$configPath = "$env:LOCALAPPDATA\Syncthing\config.xml"
[xml]$xml = Get-Content $configPath
$userHome = $env:USERPROFILE
$changed = 0
foreach ($folder in $xml.configuration.folder) {
    $oldPath = $folder.path
    if ($oldPath -match '^~[/\\]') {
        $newPath = $oldPath -replace '^~[/\\]', "$userHome\"
        $newPath = $newPath -replace '/', '\'
        $folder.path = $newPath
        $changed++
    }
}
if ($changed -gt 0) {
    $xml.Save($configPath)
    Write-Host "Paths converted successfully."
}
```

## Conflict Prevention

- Add temporary file patterns to `.stignore`
- Use chezmoi template conditionals for machine-specific config:

```go
{{ if eq .chezmoi.hostname "macbook" }}
// macbook-specific config
{{ end }}
```