@narumitw/pi-sync 0.31.0 → 0.34.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 +159 -236
- package/package.json +11 -3
- package/src/backend-factory.ts +20 -0
- package/src/command.ts +39 -39
- package/src/config-errors.ts +7 -0
- package/src/config-file.ts +64 -8
- package/src/config.ts +605 -476
- package/src/file-selection.ts +121 -140
- package/src/git-backend.ts +832 -0
- package/src/git-config.ts +136 -0
- package/src/git-runner.ts +276 -0
- package/src/git-storage.ts +288 -0
- package/src/git-ui.ts +307 -0
- package/src/manager-helpers.ts +64 -0
- package/src/manager-ui.ts +537 -542
- package/src/s3-backend.ts +498 -0
- package/src/s3-client.ts +147 -64
- package/src/s3-credentials-ui.ts +92 -0
- package/src/secret-input.ts +199 -0
- package/src/settings-management.ts +191 -277
- package/src/settings-ui.ts +94 -115
- package/src/setup-switch.ts +150 -0
- package/src/snapshot-apply.ts +13 -7
- package/src/snapshot-codec.ts +48 -0
- package/src/snapshot-transaction.ts +3 -1
- package/src/snapshot.ts +63 -32
- package/src/storage-connections-ui.ts +308 -0
- package/src/sync-backend.ts +97 -0
- package/src/sync-format.ts +31 -33
- package/src/sync-operations.ts +912 -0
- package/src/sync-policy.ts +137 -61
- package/src/sync-setups-ui.ts +177 -0
- package/src/sync-state.ts +48 -48
- package/src/sync.ts +180 -739
- package/src/types.ts +177 -53
- package/src/webdav-backend.ts +678 -0
- package/src/webdav-client.ts +390 -0
- package/src/webdav-config.ts +92 -0
- package/src/webdav-ui.ts +421 -0
- package/src/target-switch.ts +0 -119
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
|
[](https://www.npmjs.com/package/@narumitw/pi-sync) [](https://pi.dev) [](./LICENSE)
|
|
4
4
|
|
|
5
|
-
`@narumitw/pi-sync` syncs selected Pi
|
|
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
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
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
|
|
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
|
|
37
|
+
Run:
|
|
47
38
|
|
|
48
39
|
```text
|
|
49
40
|
/sync
|
|
50
41
|
```
|
|
51
42
|
|
|
52
|
-
|
|
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
|
-
|
|
45
|
+
The manager shows local state without contacting remote storage:
|
|
78
46
|
|
|
79
47
|
```text
|
|
80
|
-
Current
|
|
81
|
-
Storage: Cloudflare R2 · personal-pi
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
|
59
|
+
The canonical private user file is:
|
|
107
60
|
|
|
108
61
|
```text
|
|
109
|
-
|
|
62
|
+
~/.pi/agent/pi-sync.json
|
|
110
63
|
```
|
|
111
64
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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. On POSIX, pi-sync creates and replaces this file with mode `0600`. 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
|
-
|
|
69
|
+
### Complete version 3 example
|
|
119
70
|
|
|
120
71
|
```json
|
|
121
72
|
{
|
|
122
|
-
"version":
|
|
123
|
-
"
|
|
124
|
-
"
|
|
125
|
-
"
|
|
73
|
+
"version": 3,
|
|
74
|
+
"activeSyncSetup": "home",
|
|
75
|
+
"onSwitch": "ask-before-pull",
|
|
76
|
+
"storageConnections": {
|
|
126
77
|
"r2": {
|
|
127
|
-
"
|
|
128
|
-
"endpoint": "https
|
|
78
|
+
"type": "s3",
|
|
79
|
+
"endpoint": "https://example.r2.cloudflarestorage.com",
|
|
129
80
|
"region": "auto",
|
|
130
|
-
"
|
|
131
|
-
|
|
81
|
+
"credentials": {
|
|
82
|
+
"accessKeyId": "<access-key-id>",
|
|
83
|
+
"secretAccessKey": "<secret-access-key>"
|
|
84
|
+
}
|
|
132
85
|
},
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
"
|
|
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
|
-
"
|
|
99
|
+
"syncSetups": {
|
|
142
100
|
"home": {
|
|
143
|
-
"
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
"
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
"
|
|
153
|
-
"
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
"
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
138
|
+
### Required backend shapes
|
|
179
139
|
|
|
180
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
+
`onSwitch` accepts:
|
|
185
150
|
|
|
186
|
-
|
|
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
|
-
|
|
155
|
+
### Included content
|
|
189
156
|
|
|
190
|
-
|
|
157
|
+
`sync.include` is ordered and duplicate-free. Supported Pi roots are:
|
|
191
158
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
170
|
+
### Unsupported old settings and recovery
|
|
222
171
|
|
|
223
|
-
|
|
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
|
-
|
|
174
|
+
Recovery:
|
|
226
175
|
|
|
227
|
-
1.
|
|
228
|
-
2.
|
|
229
|
-
3.
|
|
230
|
-
4.
|
|
231
|
-
5.
|
|
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
|
|
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
|
|
186
|
+
The menu is preferred, while deterministic routes remain available:
|
|
238
187
|
|
|
239
188
|
```text
|
|
240
189
|
/sync help
|
|
241
|
-
/sync use <
|
|
190
|
+
/sync use <setup>
|
|
242
191
|
/sync init
|
|
243
|
-
/sync config [--
|
|
244
|
-
/sync files [--
|
|
245
|
-
/sync status [--
|
|
246
|
-
/sync diff [--
|
|
247
|
-
/sync doctor [--
|
|
248
|
-
/sync push [--
|
|
249
|
-
/sync pull [--
|
|
250
|
-
/sync sync [--
|
|
251
|
-
/sync history [--
|
|
252
|
-
/sync rollback <snapshot-id> [--
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
- `--
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
214
|
+
## 🔄 Backend and recovery model
|
|
270
215
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
308
|
-
- Push
|
|
309
|
-
-
|
|
310
|
-
-
|
|
311
|
-
-
|
|
312
|
-
-
|
|
313
|
-
- Terminal-bound
|
|
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
|
|
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
|
|
332
|
-
│ ├── sync.ts
|
|
333
|
-
│ ├──
|
|
334
|
-
│ ├── file
|
|
335
|
-
│ ├──
|
|
336
|
-
│ ├──
|
|
337
|
-
│ ├──
|
|
338
|
-
│ ├──
|
|
339
|
-
│ ├──
|
|
340
|
-
│
|
|
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,
|
|
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.
|
|
4
|
-
"description": "Pi extension that syncs Pi configuration through Cloudflare R2 or S3-compatible storage.",
|
|
3
|
+
"version": "0.34.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
|
+
};
|