@narumitw/pi-sync 0.31.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,27 +1,18 @@
1
- # ☁️ pi-sync — R2/S3 Pi Settings Sync
1
+ # ☁️ pi-sync — Git/WebDAV/R2/S3 Pi Settings Sync
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@narumitw/pi-sync)](https://www.npmjs.com/package/@narumitw/pi-sync) [![Pi extension](https://img.shields.io/badge/Pi-extension-blue)](https://pi.dev) [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)
4
4
 
5
- `@narumitw/pi-sync` syncs selected Pi configuration through Cloudflare R2 or other S3-compatible object storage. Named sync targets such as `home` and `work` can reuse storage profiles such as `r2` and `s3`.
6
-
7
- The extension uses immutable snapshot bundles, a `latest.json` publication pointer, local locking, secret scanning, pre-apply backups, and recoverable local apply transactions. Conversation/session syncing remains opt-in because session JSONL can contain prompts, tool output, paths, screenshots, and secrets.
5
+ `@narumitw/pi-sync` syncs selected Pi content through Git, WebDAV, Cloudflare R2, or other S3-compatible storage. A reusable **storage connection** holds access configuration. A named **sync setup** selects one connection, one exact remote storage path, included content, and automatic-sync behavior.
8
6
 
9
7
  ## ✨ Features
10
8
 
11
- - Opens a goal-oriented `/sync` manager showing the current target, storage, auto-sync, session scope, and relevant next actions.
12
- - Supports multiple named **sync targets** and reusable R2/S3 **storage profiles**.
13
- - Asks to review a pull after switching targets by default, with settings to start that review automatically or switch only.
14
- - Previews concrete local or remote file changes before push, pull, force resolution, or rollback.
15
- - Uses transactional synced-content drafts with explicit Save, Discard, and Continue editing choices.
16
- - Keeps direct `help`, `use`, `init`, `config`, `files`, `status`, `diff`, `doctor`, `push`, `pull`, `sync`, `history`, `rollback`, and `unlock` routes for compatibility and automation.
17
- - Syncs allowlisted Pi configuration from the Pi agent directory:
18
- - `settings.json`, `keybindings.json`, `models.json`, `AGENTS.md`, and `APPEND_SYSTEM.md`
19
- - recursive `skills/`, `prompts/`, `themes/`, and `extensions/` groups
20
- - safe top-level files selected through `extraFiles`
21
- - optionally denylist-filtered `sessions/**/*.jsonl`
22
- - Preserves files outside the addressed target's selection locally and in remote uploads.
23
- - Creates backups before pull/rollback and journals multi-file applies so failures roll back; interrupted transactions recover before the next sync.
24
- - Refuses common secret patterns, unsafe paths, symlink escapes, live-lock removal, and unreviewed conflict overwrites.
9
+ - A goal-oriented `/sync` manager with symmetric **Sync setups** and **Storage connections** list/detail flows.
10
+ - S3-compatible storage, a Cloudflare R2 setup preset, Git, and WebDAV.
11
+ - Immutable snapshots, secret scanning, local locks, conflict checks, pull backups, transactional apply, and recovery journals.
12
+ - Exact reviewed storage paths that do not change when a local sync setup is renamed.
13
+ - One ordered `sync.include` list for Pi roots, safe agent-relative paths, and the privacy-sensitive `sessions` root.
14
+ - Atomic private settings writes that preserve unknown fields and reject stale concurrent edits.
15
+ - Fail-closed validation for missing references, mixed backend fields, duplicate remote locations, unsafe paths, malformed credentials, and credential-bearing Git URLs.
25
16
 
26
17
  ## 📦 Install
27
18
 
@@ -35,7 +26,7 @@ Try without installing permanently:
35
26
  pi -e npm:@narumitw/pi-sync
36
27
  ```
37
28
 
38
- Try this package from a local checkout:
29
+ Try a local checkout:
39
30
 
40
31
  ```bash
41
32
  pi -e ./extensions/pi-sync
@@ -43,311 +34,243 @@ pi -e ./extensions/pi-sync
43
34
 
44
35
  ## 🚀 Quick start
45
36
 
46
- Run the manager:
37
+ Run:
47
38
 
48
39
  ```text
49
40
  /sync
50
41
  ```
51
42
 
52
- When pi-sync is not configured, choose **Set up sync**. The TUI guides you through:
53
-
54
- 1. Cloudflare R2 or another S3-compatible service
55
- 2. `Personal / Home`, `Work`, or a custom target purpose
56
- 3. an endpoint and, for S3, the existing bucket name
57
- 4. a recommended remote location or advanced customization
58
- 5. environment credentials or a private settings-file template
59
- 6. a synced-content preset
60
- 7. an exact setup preview and **Save setup** confirmation
61
-
62
- For a first Cloudflare R2 target, the recommended location requires no raw path questions:
63
-
64
- ```json
65
- {
66
- "profile": "r2",
67
- "bucket": "pi-sync",
68
- "prefix": "pi-sync",
69
- "namespace": "home"
70
- }
71
- ```
72
-
73
- The R2 bucket must already exist; pi-sync never creates buckets. Generic S3 setup asks for one existing, potentially globally unique bucket and derives storage profile `s3`, prefix `pi-sync`, and namespace `home` or `work`. **Customize remote location** retains direct control over profile name, bucket, prefix, and namespace.
74
-
75
- Pi's extension input API does not provide masked secret entry, so pi-sync never asks for a secret in an unmasked dialog. Use existing environment credentials or add credentials manually to the private settings file. Secret values are never shown in menus, status, warnings, or errors.
43
+ Choose **Set up sync**, then review the storage connection, exact storage path, included content, automatic-sync choice, and masked credentials before saving. Buckets and remote repositories must already exist. Git uses the user's existing non-interactive SSH or credential-helper configuration and never stores Git credentials.
76
44
 
77
- A configured manager shows:
45
+ The manager shows local state without contacting remote storage:
78
46
 
79
47
  ```text
80
- Current target: home
81
- Storage: Cloudflare R2 · personal-pi
82
- Synced content: 9 built-in groups · 0 extra files · Sessions: Off
83
- Auto-sync: On
84
- Last applied snapshot: snapshot-id (or Never synced)
85
- Remote changes: Not checked
48
+ Current sync setup: home
49
+ Storage: Cloudflare R2 · r2 · personal-pi
50
+ Included: 5 built-in groups · 0 extra files · Sessions off
51
+ Automatic sync: On
52
+ Remote status: Not checked
86
53
  ```
87
54
 
88
- Its primary actions are:
89
-
90
- - **Sync now (recommended)** — conservatively decide whether to push, pull, or do nothing
91
- - **Pull from remote** — check the remote snapshot, preview exact local writes/deletes, then confirm
92
- - **Push to remote** — scan and preview exact remote publication changes, then confirm
93
- - **Switch target** — preview the target change, then follow the configured pull behavior
94
- - **Status & changes** — perform a cancellable read-only remote check
95
- - **Settings** — control post-switch pulling, automatic sync, and synced content
96
- - **More…** — open one shallow level containing target/storage management, history/recovery, Help, and Back
97
-
98
- The menu does not query remote storage merely to open. It shows the last locally applied snapshot and marks remote changes as unchecked until an operation runs. When no synced content is selected, transfer actions are hidden and Settings becomes the first action. During an active or recoverable lock, mutation actions remain unavailable.
99
-
100
- Push and pull keep their existing concrete previews and confirmation prompts. Pull creates a local backup before applying; push scans locally managed content for likely secrets before publication. Escape cancels a pre-commit check without changing local or remote files. Once publication or apply begins, cancellation is disabled. Conflicts never force automatically; inspect **Status & changes** and use an explicit direct route only after reviewing the conflict.
101
-
102
- Escape exits the main menu. **More…** and secondary menus provide Back; dirty synced-content drafts provide Save, Discard, and Continue editing. Cancellation before publication has no side effects.
55
+ Primary actions include **Sync now**, **Switch sync setup**, **Status & changes**, **Settings**, and **More…**. Secondary screens provide **Back**; Escape closes the flow. Destructive and externally visible operations show exact previews and confirmations.
103
56
 
104
57
  ## ⚙️ Settings
105
58
 
106
- The private user settings file is:
59
+ The canonical private user file is:
107
60
 
108
61
  ```text
109
- ${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-sync.json
62
+ ~/.pi/agent/pi-sync.json
110
63
  ```
111
64
 
112
- On first read, an existing `pi-sync.local.json` is migrated byte-for-byte to `pi-sync.json`
113
- with private POSIX `0600` permissions. The private legacy file is retained as a recovery copy and may
114
- be deleted after verifying the new file. If both files exist, `pi-sync.json` takes precedence and the
115
- legacy file remains untouched. Malformed, invalid, symlinked, changed, or otherwise unsafe legacy
116
- files are never overwritten automatically.
65
+ Pi's configured agent directory replaces `~/.pi/agent` when applicable. Missing settings load as unconfigured without creating an agent directory, file, temporary, or lock. An explicit setup creates the file atomically. On POSIX, pi-sync creates and replaces it with mode `0600`. Pi-sync processes coordinate settings access through `pi-sync.json.mutation-lock`; lock-unaware editors are outside that serialization boundary and should not save the file while a pi-sync settings operation is running. Credentials stay in this canonical private file and are never shown in menus, reviews, status, notifications, errors, logs, or completion metadata.
66
+
67
+ An existing private `pi-sync.local.json` containing a valid version 3 document is copied byte-for-byte to `pi-sync.json`; the old file remains as a recovery copy. If both paths exist, `pi-sync.json` wins and the other path remains untouched.
117
68
 
118
- A version 2 example:
69
+ ### Complete version 3 example
119
70
 
120
71
  ```json
121
72
  {
122
- "version": 2,
123
- "activeTarget": "home",
124
- "targetSwitchAction": "ask",
125
- "profiles": {
73
+ "version": 3,
74
+ "activeSyncSetup": "home",
75
+ "onSwitch": "ask-before-pull",
76
+ "storageConnections": {
126
77
  "r2": {
127
- "kind": "r2",
128
- "endpoint": "https://<account-id>.r2.cloudflarestorage.com",
78
+ "type": "s3",
79
+ "endpoint": "https://example.r2.cloudflarestorage.com",
129
80
  "region": "auto",
130
- "accessKeyId": "<access-key-id>",
131
- "secretAccessKey": "<secret-access-key>"
81
+ "credentials": {
82
+ "accessKeyId": "<access-key-id>",
83
+ "secretAccessKey": "<secret-access-key>"
84
+ }
132
85
  },
133
- "s3": {
134
- "kind": "s3-compatible",
135
- "endpoint": "https://s3.example.com",
136
- "region": "ap-northeast-1",
137
- "accessKeyId": "<access-key-id>",
138
- "secretAccessKey": "<secret-access-key>"
86
+ "github": {
87
+ "type": "git",
88
+ "remote": "git@github.com:owner/private-pi-sync.git"
89
+ },
90
+ "nextcloud": {
91
+ "type": "webdav",
92
+ "url": "https://cloud.example.com/remote.php/dav/files/user",
93
+ "credentials": {
94
+ "username": "user",
95
+ "password": "<app-password>"
96
+ }
139
97
  }
140
98
  },
141
- "targets": {
99
+ "syncSetups": {
142
100
  "home": {
143
- "profile": "r2",
144
- "bucket": "personal-pi",
145
- "prefix": "pi-sync",
146
- "namespace": "home",
147
- "autoSync": true,
148
- "syncFiles": ["settings.json", "skills", "prompts", "themes"],
149
- "syncSessions": false,
150
- "extraFiles": []
101
+ "storage": {
102
+ "connection": "r2",
103
+ "bucket": "personal-pi",
104
+ "path": "pi-sync/home"
105
+ },
106
+ "sync": {
107
+ "include": ["settings.json", "AGENTS.md", "skills", "prompts", "themes"],
108
+ "automatic": true
109
+ }
110
+ },
111
+ "git-backup": {
112
+ "storage": {
113
+ "connection": "github",
114
+ "branch": "pi-sync/home",
115
+ "path": "pi-sync/home"
116
+ },
117
+ "sync": {
118
+ "include": ["settings.json", "AGENTS.md"],
119
+ "automatic": false
120
+ }
151
121
  },
152
- "work": {
153
- "profile": "s3",
154
- "bucket": "company-pi",
155
- "prefix": "developers/narumi",
156
- "namespace": "work",
157
- "autoSync": true,
158
- "syncFiles": ["AGENTS.md", "prompts"],
159
- "syncSessions": false,
160
- "extraFiles": []
122
+ "webdav-backup": {
123
+ "storage": {
124
+ "connection": "nextcloud",
125
+ "path": "pi-sync/home"
126
+ },
127
+ "sync": {
128
+ "include": ["settings.json", "sessions"],
129
+ "automatic": false
130
+ }
161
131
  }
162
132
  }
163
133
  }
164
134
  ```
165
135
 
166
- ### Terminology
167
-
168
- - A **storage profile** owns connection/authentication fields: `kind`, `endpoint`, `region`, `accessKeyId`, `secretAccessKey`, and optional `sessionToken`.
169
- - A **sync target** references a storage profile and owns `bucket`, `prefix`, `namespace`, `autoSync`, and its synced-content policy.
170
- - `activeTarget` is used by bare commands and automatic sync.
171
- - `targetSwitchAction` controls what happens after a target switch: `ask` (default), `pull`, or `switch-only`.
172
- - `namespace` is the old flat `profile` concept. The remote layout and snapshot wire field remain named `profiles`/`profile` for compatibility.
173
-
174
- When adding another target with the same storage profile, pi-sync recommends reusing the current target's bucket and prefix while deriving a separate namespace from the new target name. For example, `home` and `work` may both use profile `r2`, bucket `pi-sync`, and prefix `pi-sync`, producing `pi-sync/profiles/home/` and `pi-sync/profiles/work/`.
175
-
176
- Two targets may intentionally overlap local files, but only one is automatic. pi-sync rejects duplicate effective target remote identities—including deprecated bucket/prefix/namespace environment overrides—to prevent independent local states from controlling the same remote pointer. Removing a target/profile removes local configuration only; it never deletes buckets or snapshots. A referenced profile cannot be removed, and users must switch away before removing one of several current targets.
136
+ Cloudflare R2 is persisted as `"type": "s3"`; R2 is a setup preset, not another schema type. Temporary S3 credentials may additionally include `credentials.sessionToken`.
177
137
 
178
- ### Target switching
138
+ ### Required backend shapes
179
139
 
180
- The default `targetSwitchAction: "ask"` changes `activeTarget` atomically, then asks in TUI mode whether to review a pull for that target. Choosing not to review leaves local files unchanged. Accepting starts the normal pull flow, which fetches the remote snapshot and shows the exact writes and deletions before apply. In print, JSON, or RPC mode, `ask` switches without pulling and directs interactive users to `/sync pull` where notifications are available.
140
+ - **S3/R2 connection:** `type`, `endpoint`, `region`, and `credentials.accessKeyId` / `credentials.secretAccessKey`.
141
+ - **S3/R2 setup storage:** `connection`, `bucket`, and complete relative `path`; `branch` is rejected.
142
+ - **Git connection:** `type` and a credential-free SSH or HTTPS `remote`.
143
+ - **Git setup storage:** `connection`, `branch`, and complete repository `path`; `bucket` is rejected.
144
+ - **WebDAV connection:** `type`, HTTPS `url`, and `credentials.username` / `credentials.password`.
145
+ - **WebDAV setup storage:** `connection` and complete relative `path`; `bucket` and `branch` are rejected.
181
146
 
182
- Set `targetSwitchAction` to `"pull"` to start that reviewed pull automatically after a confirmed target switch. The exact pull summary and apply confirmation remain enabled, conflict detection still stops unsafe merges, a local backup is created before apply, and pi-sync never adds `--force`; Pi may separately ask whether to reload changed resources after a successful pull. Because that confirmation requires observable UI, `/sync use` rejects `"pull"` in print and JSON modes before changing `activeTarget`; use TUI or RPC mode instead. Set it to `"switch-only"` to retain the previous no-pull behavior. Selecting an already-current target is a no-op. If a pull fails or its concrete review is declined, the new target remains active and pi-sync reports that synced files were not changed before `/sync pull` is retried.
147
+ Every setup requires `sync.include` and explicit `sync.automatic`. `activeSyncSetup` must reference an own-property setup when any setups exist and must be absent when the setup catalog is empty. A referenced connection cannot be removed. The current setup must be switched before removal. Two setups cannot resolve to the same normalized backend location.
183
148
 
184
- ### Synced content
149
+ `onSwitch` accepts:
185
150
 
186
- The **Settings → Choose synced content** screen edits a draft. Arrow keys navigate, Enter or Space toggles, and Escape opens the Save/Discard/Continue decision. A single Save atomically updates the target while preserving unknown fields and POSIX `0600` permissions. Saving settings never starts network sync.
151
+ - `ask-before-pull` — switch, then ask in TUI whether to start a reviewed pull;
152
+ - `pull-after-switch` — require observable UI and start the normal reviewed pull;
153
+ - `switch-only` — switch without reading or applying remote content.
187
154
 
188
- Omitting `syncFiles` preserves the legacy default of every built-in item. An empty array is valid and means the target manages nothing; **Sync now** reports that state instead of publishing an empty sync. Unknown built-in names fail validation.
155
+ ### Included content
189
156
 
190
- Unselected items are unmanaged: pi-sync does not compare, pull, overwrite, or delete their local content, and pushes preserve their existing remote content.
157
+ `sync.include` is ordered and duplicate-free. Supported Pi roots are:
191
158
 
192
- ### Environment precedence and deprecation
193
-
194
- The existing `PI_SYNC_*` family still works in this release with its existing highest precedence, but it is deprecated and will be removed in a future major version. pi-sync shows variable names—not values—and migration guidance when any are active.
195
-
196
- Move these fields into a storage profile:
197
-
198
- - `PI_SYNC_ENDPOINT`
199
- - `PI_SYNC_REGION`
200
- - `PI_SYNC_ACCESS_KEY_ID`
201
- - `PI_SYNC_SECRET_ACCESS_KEY`
202
- - `PI_SYNC_SESSION_TOKEN`
203
-
204
- Move these fields into a sync target:
205
-
206
- - `PI_SYNC_BUCKET`
207
- - `PI_SYNC_PROFILE` → `namespace`
208
- - `PI_SYNC_PREFIX`
209
- - `PI_SYNC_AUTO_SYNC`
210
- - `PI_SYNC_SESSIONS`
211
-
212
- `PI_SYNC_PROFILE` continues to mean only the remote namespace during the deprecation period; it never selects a storage profile or current target.
159
+ ```text
160
+ settings.json, keybindings.json, models.json, AGENTS.md, APPEND_SYSTEM.md,
161
+ skills, prompts, themes, extensions, sessions
162
+ ```
213
163
 
214
- Standard compatibility aliases remain below `PI_SYNC_*` precedence:
164
+ Safe agent-relative custom files or directories may also be included. Absolute paths, `..`, backslashes, controls, denied secret/settings paths, duplicate case variants, and ambiguous nested paths under reserved roots are rejected.
215
165
 
216
- - `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, `AWS_REGION`
217
- - `R2_ENDPOINT`, `R2_BUCKET`
166
+ An empty array is valid. It means no useful transfer is selected: **Sync now** reports the condition and does not claim that the setup is up to date. Unselected content remains unmanaged locally and is preserved when republishing existing remote snapshots.
218
167
 
219
- Environment-overridden fields are read-only in interactive settings. Cloudflare R2 static keys commonly reject `X-Amz-Security-Token`; pi-sync retains its one-time retry without the configured token for that R2 response.
168
+ Adding `sessions` requires a privacy acknowledgement in interactive flows. Session JSONL can contain prompts, tool output, file paths, images, and secrets. Automatic apply protects the currently open session file; restart Pi or resume a pulled session to use newly synchronized conversations.
220
169
 
221
- ### Legacy flat settings
170
+ ### Unsupported old settings and recovery
222
171
 
223
- Existing flat settings continue to work unchanged as a synthetic `default` target/profile, including settings migrated from the legacy `pi-sync.local.json` filename. The old flat `profile` remains the remote namespace.
172
+ Version 1, version 2, and non-empty unversioned documents are intentionally unsupported by this breaking schema reset. pi-sync does **not** migrate, partially interpret, downgrade, or overwrite them. Automatic sync pauses and reports an actionable version 3 error without displaying secrets.
224
173
 
225
- Adding a second target offers an explicit migration preview. On confirmation pi-sync:
174
+ Recovery:
226
175
 
227
- 1. takes an exact private backup of the original JSON bytes;
228
- 2. preserves unknown top-level fields;
229
- 3. maps existing connection and policy fields without changing the remote destination;
230
- 4. adopts the existing local sync state for the migrated target;
231
- 5. publishes the version 2 file atomically.
176
+ 1. retain the old file byte-for-byte;
177
+ 2. move it aside manually;
178
+ 3. create a new version 3 document or run the setup manager;
179
+ 4. run `/sync doctor`, inspect the exact storage path, and review the first pull or push;
180
+ 5. restore the retained file and a compatible older package only if rolling back.
232
181
 
233
- Malformed, invalid, symlinked, or concurrently changed settings are never overwritten automatically. Version 2 settings require the multi-target pi-sync release or newer. For a manual downgrade, restore the exact `.legacy-<timestamp>.bak` flat-config backup; no remote migration or rollback is needed.
182
+ Malformed, invalid, unsupported, symlinked, or concurrently changed documents remain untouched. Failed UI saves keep the previous file and displayed/effective state.
234
183
 
235
184
  ## 💬 Commands
236
185
 
237
- The menu is the preferred interactive workflow. Existing deterministic routes remain available:
186
+ The menu is preferred, while deterministic routes remain available:
238
187
 
239
188
  ```text
240
189
  /sync help
241
- /sync use <target>
190
+ /sync use <setup>
242
191
  /sync init
243
- /sync config [--target <name>]
244
- /sync files [--target <name>]
245
- /sync status [--target <name>]
246
- /sync diff [--target <name>]
247
- /sync doctor [--target <name>]
248
- /sync push [--target <name>]
249
- /sync pull [--target <name>]
250
- /sync sync [--target <name>]
251
- /sync history [--target <name>]
252
- /sync rollback <snapshot-id> [--target <name>]
192
+ /sync config [--setup <name>]
193
+ /sync files [--setup <name>]
194
+ /sync status [--setup <name>]
195
+ /sync diff [--setup <name>]
196
+ /sync doctor [--setup <name>]
197
+ /sync push [--setup <name>]
198
+ /sync pull [--setup <name>]
199
+ /sync sync [--setup <name>]
200
+ /sync history [--setup <name>]
201
+ /sync rollback <snapshot-id> [--setup <name>]
253
202
  /sync unlock --stale
254
203
  ```
255
204
 
256
- Flags:
257
-
258
- - `--target <name>` addresses a target without switching it.
259
- - `--yes` / `-y` skips an explicit direct command's confirmation.
260
- - `--force` resolves a reviewed conflict for push/pull/sync.
205
+ - `--setup <name>` addresses a setup without switching it.
206
+ - `--yes` / `-y` skips a direct route's confirmation.
207
+ - `--force` accepts a reviewed content conflict but never disables backend concurrency protection.
261
208
  - `--stale` requests guarded stale-lock recovery.
262
209
 
263
- Unknown flags, unexpected values, and missing target/snapshot values are rejected. Argument completion includes configured target names after session start.
264
-
265
- TUI mode provides the full manager and custom settings components. RPC uses Pi's dialog/notification protocol and does not call TUI-only components. Print/JSON modes cannot display extension UI; use explicit direct routes for compatible automation and do not expect notification-only status output.
210
+ The former version 2 setup-addressing flag is rejected. Unknown flags, unknown commands, trailing values, and missing setup/snapshot values are rejected. Completion includes known setup names and preserves preceding command tokens.
266
211
 
267
- ## 🔄 Sync and recovery model
212
+ TUI mode provides custom components. RPC uses Pi's supported dialog/notification protocol. Print and JSON modes never enter TUI-only screens; unsupported interactive routes reject or remain protocol-safe rather than relying on no-op output.
268
213
 
269
- For the current target, startup auto-sync uses conservative decisions:
214
+ ## 🔄 Backend and recovery model
270
215
 
271
- - local changed or remote empty → preview/confirm manually, or push in the existing quiet automatic path
272
- - identical first sync → initialize local state
273
- - safe remote-only change → pull with backup
274
- - first-sync mismatch or both changed → stop and require review
275
- - no selected content → report/skip
216
+ | Backend | Publication guarantee | Authentication | Remote path |
217
+ | --- | --- | --- | --- |
218
+ | Git | Exact expected-ref lease | Existing SSH/configured credential helper | `<branch>:<storage.path>` |
219
+ | WebDAV | Verified strong conditional requests | Private settings username/app password | `<url>/<storage.path>` |
220
+ | R2/S3 | Read-check-write-verify | Private settings credentials | `<bucket>/<storage.path>` |
276
221
 
277
- Switching targets first changes `activeTarget`, then follows `targetSwitchAction`: ask before reviewing a pull in TUI by default, start a reviewed pull automatically when configured, or stop after switching. Pulls remain pinned to the selected target and retain exact summaries, locking, backups, and conflict safeguards. The next startup uses the new target's `autoSync` regardless of the switch action.
222
+ Git requires Git 2.30 or newer and a SHA-1-format remote repository. HTTPS userinfo, URL passwords, local paths, `file`, `git`, `ext`, and remote-helper transports are rejected. When editing a Git setup, changing its storage path also requires a new owned branch so the existing branch remains readable at its reviewed path. The private bare cache under `.pisync/git/` is rebuildable.
278
223
 
279
- Remote layout remains compatible:
224
+ WebDAV requires HTTPS except loopback tests. URL credentials, query strings, fragments, unsafe redirects, weak/missing ETags, and ignored conditional headers fail closed. `/sync doctor` verifies collection and conditional-write behavior with an isolated probe.
280
225
 
281
- ```text
282
- <prefix>/
283
- └── profiles/
284
- └── <namespace>/
285
- ├── latest.json
286
- ├── history.json
287
- └── snapshots/
288
- └── <snapshot-id>.json.gz
289
- ```
290
-
291
- Snapshot upload is staging; `latest.json` is the active publication boundary. A failed history update after publication is reported as “snapshot active, history needs repair” instead of falsely claiming no remote change.
292
-
293
- Before pull/rollback, pi-sync writes a backup under `.pisync/backups/`. It then preflights all paths, stages a private transaction journal, applies changes, and restores every affected path if a later mutation fails. An interrupted journal is recovered before the next session/snapshot apply. Filesystem-wide replacement cannot be one OS primitive, so the guarantee is a complete previous or complete new accepted state after rollback/recovery—not an unrecoverable partial accepted state.
294
-
295
- R2/S3 does not provide the true cross-machine compare-and-swap used here. pi-sync re-reads `latest.json` immediately before publication and verifies afterward, but simultaneous writers can still race. Review status before important forced updates.
226
+ S3/R2 stages immutable bundles, rechecks the visible head before publication, and verifies afterward. Unlike Git/WebDAV, generic S3 does not provide an atomic compare-and-swap for `latest.json`; status review remains important for simultaneous writers.
296
227
 
297
- ## 🔒 Session syncing
298
-
299
- `syncSessions` defaults to `false`. Enabling it includes configured Pi session JSONL files and makes the privacy warning visible in settings, status, previews, and confirmations.
300
-
301
- Only JSONL session files are included. Denylisted names and paths such as `.env*`, `.pisync`, `node_modules`, `token`, and `secret` are ignored. The currently open session file is protected during pull; restart Pi or resume a pulled session to use newly synced conversations.
302
-
303
- Sessions can contain prompts, model output, tool results, file paths, images, and secrets. Use only storage you trust.
228
+ Before pull or rollback, pi-sync writes a backup under `.pisync/backups/`. Apply preflights paths and checksums, journals all mutations, restores the prior state after failures, and recovers interrupted journals on startup. Removing a local setup or connection never deletes remote data.
304
229
 
305
230
  ## 🛡️ Safety notes
306
231
 
307
- - Credentials stay local; canonical, legacy, temporary, and migration-recovery settings files are always excluded from snapshots.
308
- - Push refuses common secret patterns in locally managed files.
309
- - Pull/rollback reject unsafe paths, duplicate paths, checksum mismatches, symlink parents, and file/directory replacement hazards before mutation.
310
- - Unmanaged local/remote files remain preserved.
311
- - A live local lock disables destructive work. Stale/unreadable lock recovery retains process-liveness and guard checks.
312
- - Force operations remain explicit direct/conflict-recovery actions and retain exact confirmations.
313
- - Terminal-bound config, remote metadata, machine names, paths, and errors are control-character sanitized.
232
+ - Canonical, legacy, temporary, and recovery settings paths are denied from snapshots.
233
+ - Push scans managed local content for common secret patterns.
234
+ - Remote snapshot references, checksums, paths, manifests, response sizes, and publication revisions are validated.
235
+ - Symlink parents, path escapes, duplicate paths, and unsafe file/directory replacement fail before local mutation.
236
+ - Live locks block mutation; stale recovery rechecks process and guard ownership.
237
+ - Cancellation aborts preparation and dialogs. Publication/apply commit boundaries finish with bounded signals and report ambiguous outcomes explicitly.
238
+ - Terminal-bound names, paths, metadata, and errors are control-character sanitized.
314
239
 
315
240
  ## ♿ Conditional terminal accessibility smoke
316
241
 
317
- Pi exposes terminal components rather than a semantic/ARIA tree, so automated tests verify textual state, keyboard paths, control-character escaping, and 32/60/100-column fitting rather than claiming semantic announcements. When validating a release in a supported environment:
318
-
319
- 1. open `/sync` using only the keyboard and verify arrows, Enter, Space, search typing, paging where available, Back, and Escape;
320
- 2. enter a Unicode target/profile name with an IME and confirm focus returns to Pi after closing;
321
- 3. check the main, warning, preview, saved, cancelled, and invalid states with the terminal screen reader in use;
322
- 4. repeat under a light and dark Pi theme and at 32, 60, and 100 columns.
323
-
324
- Critical meaning is always present in text such as `(current)`, `Warning`, `Invalid`, `Saved`, `Cancelled`, or `Applied`; color is supplementary.
242
+ Pi exposes terminal components rather than a semantic/ARIA tree. Release validation checks textual state, keyboard operation, Escape/Back, control escaping, and narrow rendering. Critical meaning appears in text such as `(current)`, `Warning`, `Invalid`, `Saved`, `Cancelled`, and `Applied`; color is supplementary.
325
243
 
326
244
  ## 🗂️ Package layout
327
245
 
328
246
  ```text
329
247
  extensions/pi-sync/
330
248
  ├── src/
331
- │ ├── index.ts # Thin Pi entrypoint
332
- │ ├── sync.ts # Command and lifecycle orchestration
333
- │ ├── manager-ui.ts # Goal-oriented menus and setup/management flows
334
- │ ├── file-selection.ts # Transactional synced-content editor
335
- │ ├── config-file.ts # Private settings I/O and legacy filename migration
336
- │ ├── settings-management.ts # Profiles, targets, migration, and atomic saves
337
- │ ├── settings-ui.ts # SettingsList interaction and serialized saves
338
- │ ├── target-switch.ts # Post-switch prompt, policy, and target handoff
339
- │ ├── snapshot-transaction.ts # Local apply journal, rollback, and recovery
340
- │ └── *.ts # Config, policy, snapshot, S3, lock, and format modules
249
+ │ ├── index.ts
250
+ │ ├── sync.ts
251
+ │ ├── config.ts
252
+ │ ├── config-file.ts
253
+ │ ├── settings-management.ts
254
+ │ ├── manager-ui.ts
255
+ │ ├── storage-connections-ui.ts
256
+ │ ├── sync-setups-ui.ts
257
+ │ ├── file-selection.ts
258
+ │ ├── sync-operations.ts
259
+ │ ├── sync-backend.ts
260
+ │ ├── s3-backend.ts
261
+ │ ├── webdav-backend.ts
262
+ │ ├── git-backend.ts
263
+ │ ├── snapshot.ts
264
+ │ └── *.ts
341
265
  ├── test/
342
266
  ├── README.md
343
267
  ├── LICENSE
344
- ├── tsconfig.json
345
268
  └── package.json
346
269
  ```
347
270
 
348
271
  ## 🔎 Keywords
349
272
 
350
- Pi extension, Pi coding agent, settings sync, Cloudflare R2, S3-compatible storage, multi-profile sync, sync targets, snapshot sync, dotfiles sync.
273
+ Pi extension, Pi coding agent, settings sync, Git, WebDAV, Nextcloud, Cloudflare R2, S3-compatible storage, storage connections, sync setups, snapshot sync, dotfiles sync.
351
274
 
352
275
  ## 📄 License
353
276
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@narumitw/pi-sync",
3
- "version": "0.31.0",
4
- "description": "Pi extension that syncs Pi configuration through Cloudflare R2 or S3-compatible storage.",
3
+ "version": "0.35.0",
4
+ "description": "Pi extension that syncs Pi configuration through Git, WebDAV, Cloudflare R2, or S3-compatible storage.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "private": false,
@@ -11,7 +11,14 @@
11
11
  "pi",
12
12
  "sync",
13
13
  "r2",
14
- "s3"
14
+ "s3",
15
+ "webdav",
16
+ "nextcloud",
17
+ "synology",
18
+ "git",
19
+ "github",
20
+ "gitlab",
21
+ "forgejo"
15
22
  ],
16
23
  "files": [
17
24
  "src",
@@ -29,6 +36,7 @@
29
36
  "typecheck": "tsc --noEmit"
30
37
  },
31
38
  "dependencies": {
39
+ "fast-xml-parser": "^5.10.1",
32
40
  "proper-lockfile": "^4.1.2"
33
41
  },
34
42
  "devDependencies": {
@@ -0,0 +1,20 @@
1
+ import { GitSyncBackend } from "./git-backend.js";
2
+ import { S3SyncBackend } from "./s3-backend.js";
3
+ import type { SyncBackend } from "./sync-backend.js";
4
+ import type { AnySyncConfig } from "./types.js";
5
+ import { WebDavSyncBackend } from "./webdav-backend.js";
6
+
7
+ export type SyncBackendFactory = {
8
+ bivarianceHack(config: AnySyncConfig): SyncBackend;
9
+ }["bivarianceHack"];
10
+
11
+ export const createSyncBackend: SyncBackendFactory = (config) => {
12
+ switch (config.backend.type) {
13
+ case "s3":
14
+ return new S3SyncBackend(config.backend);
15
+ case "webdav":
16
+ return new WebDavSyncBackend(config.backend);
17
+ case "git":
18
+ return new GitSyncBackend(config.backend);
19
+ }
20
+ };